VERSION 4.00
Begin VB.Form frmTable4P 
   BorderStyle     =   1  'Fixed Single
   Caption         =   "The 4 part Program"
   ClientHeight    =   8955
   ClientLeft      =   1125
   ClientTop       =   1620
   ClientWidth     =   9060
   Height          =   9360
   Left            =   1065
   LinkTopic       =   "Form1"
   MaxButton       =   0   'False
   MinButton       =   0   'False
   ScaleHeight     =   8955
   ScaleWidth      =   9060
   Top             =   1275
   Width           =   9180
   Begin VB.CommandButton cmdNextFile 
      Caption         =   "Next &file"
      Height          =   375
      Left            =   120
      TabIndex        =   52
      Top             =   1320
      Width           =   855
   End
   Begin VB.TextBox txtDnote 
      Height          =   285
      Left            =   8280
      TabIndex        =   51
      Top             =   3975
      Width           =   616
   End
   Begin VB.TextBox txtTempo 
      Height          =   285
      Left            =   968
      TabIndex        =   49
      Top             =   2167
      Width           =   495
   End
   Begin VB.CommandButton cmdWriteAll 
      Caption         =   "all"
      Height          =   495
      Left            =   3480
      TabIndex        =   48
      Top             =   7320
      Width           =   375
   End
   Begin VB.CommandButton cmdShowAccid 
      Caption         =   "Show &key #,b"
      Height          =   495
      Left            =   1800
      TabIndex        =   47
      Top             =   7320
      Width           =   735
   End
   Begin VB.CommandButton cmdValidCand 
      Caption         =   "&Valid cands"
      Height          =   495
      Left            =   1095
      TabIndex        =   46
      Top             =   7320
      Width           =   615
   End
   Begin VB.CommandButton cmdWriteCsv 
      Caption         =   "Write &CSV"
      Height          =   495
      Left            =   4800
      TabIndex        =   45
      Top             =   7320
      Width           =   615
   End
   Begin VB.TextBox txtNote 
      Alignment       =   2  'Center
      Height          =   285
      Left            =   7080
      Locked          =   -1  'True
      TabIndex        =   44
      Text            =   "0"
      Top             =   240
      Width           =   495
   End
   Begin VB.TextBox txtMeas 
      Alignment       =   2  'Center
      Height          =   285
      Left            =   6600
      Locked          =   -1  'True
      TabIndex        =   43
      Text            =   "1"
      Top             =   240
      Width           =   495
   End
   Begin VB.CommandButton cmdPrevMsg 
      Caption         =   "Pre&v msg"
      Height          =   255
      Left            =   120
      TabIndex        =   40
      Top             =   7440
      Width           =   855
   End
   Begin VB.CommandButton cmdNextMsg 
      Caption         =   "Nex&t msg"
      Height          =   255
      Left            =   120
      TabIndex        =   39
      Top             =   7080
      Width           =   855
   End
   Begin VB.TextBox txtPrint 
      Height          =   975
      Left            =   120
      Locked          =   -1  'True
      MultiLine       =   -1  'True
      ScrollBars      =   2  'Vertical
      TabIndex        =   35
      Top             =   7920
      Width           =   8415
   End
   Begin VB.TextBox txtMyCand 
      Alignment       =   2  'Center
      Height          =   285
      Left            =   7200
      TabIndex        =   34
      Top             =   3960
      Width           =   375
   End
   Begin VB.TextBox txtCandSolNo 
      Alignment       =   2  'Center
      Height          =   285
      Left            =   360
      TabIndex        =   32
      Top             =   6720
      Width           =   375
   End
   Begin VB.CommandButton cmdPrevSolution 
      Caption         =   "Prev S&olution"
      Height          =   495
      Left            =   240
      TabIndex        =   30
      Top             =   5760
      Width           =   735
   End
   Begin VB.CommandButton cmdNextSolution 
      Caption         =   "Next So&lution"
      Height          =   495
      Left            =   120
      TabIndex        =   29
      Top             =   5160
      Width           =   735
   End
   Begin VB.CommandButton cmdPrevNote 
      Caption         =   "P&rev Note"
      Height          =   495
      Left            =   360
      TabIndex        =   28
      Top             =   4560
      Width           =   615
   End
   Begin VB.CommandButton cmdNextNote 
      Caption         =   "N&ext Note"
      Height          =   495
      Left            =   120
      TabIndex        =   27
      Top             =   3960
      Width           =   615
   End
   Begin VB.TextBox txtGoodCand 
      Alignment       =   2  'Center
      Height          =   285
      Left            =   5160
      Locked          =   -1  'True
      TabIndex        =   26
      Top             =   3960
      Width           =   375
   End
   Begin VB.TextBox txtBest 
      Alignment       =   2  'Center
      Height          =   285
      Left            =   6240
      Locked          =   -1  'True
      TabIndex        =   24
      Top             =   3960
      Width           =   375
   End
   Begin VB.TextBox txtNoCands 
      Alignment       =   2  'Center
      Height          =   285
      Left            =   3720
      Locked          =   -1  'True
      TabIndex        =   22
      Top             =   3960
      Width           =   495
   End
   Begin VB.TextBox txtNoteNo 
      Alignment       =   2  'Center
      DataSource      =   "gNoteX"
      Height          =   285
      Left            =   1920
      TabIndex        =   20
      Top             =   3960
      Width           =   495
   End
   Begin VB.CommandButton cmdShow 
      Caption         =   "&Show harmony"
      Height          =   495
      Left            =   5640
      TabIndex        =   18
      Top             =   7320
      Width           =   737
   End
   Begin VB.CommandButton cmdWriteAbc 
      Caption         =   "1 song to &ABC"
      Height          =   495
      Left            =   2640
      TabIndex        =   15
      Top             =   7320
      Width           =   735
   End
   Begin VB.CommandButton cmdHarmony 
      Caption         =   "Har &mony"
      Height          =   495
      Left            =   7680
      TabIndex        =   14
      Top             =   7320
      Width           =   495
   End
   Begin VB.CommandButton cmdChords 
      Caption         =   "Chor&ds"
      Height          =   495
      Left            =   8280
      TabIndex        =   13
      Top             =   7320
      Width           =   615
   End
   Begin VB.CommandButton cmdPrevSong 
      Caption         =   "&Prev song"
      Height          =   495
      Left            =   360
      TabIndex        =   12
      Top             =   3360
      Width           =   615
   End
   Begin VB.CommandButton cmdNextSong 
      Caption         =   "&Next song"
      Height          =   495
      Left            =   120
      TabIndex        =   11
      Top             =   2760
      Width           =   615
   End
   Begin VB.TextBox txtLength 
      Alignment       =   2  'Center
      Height          =   285
      Left            =   120
      Locked          =   -1  'True
      TabIndex        =   8
      Top             =   2400
      Width           =   1335
   End
   Begin VB.TextBox txtMeter 
      Alignment       =   2  'Center
      Height          =   285
      Left            =   120
      Locked          =   -1  'True
      TabIndex        =   7
      Top             =   2160
      Width           =   615
   End
   Begin VB.TextBox txtDesc 
      Height          =   375
      Left            =   2280
      Locked          =   -1  'True
      TabIndex        =   6
      Text            =   "Non Nobis                                         "
      Top             =   120
      Width           =   4215
   End
   Begin VB.TextBox txtSongNum 
      Alignment       =   2  'Center
      Height          =   360
      Left            =   1800
      TabIndex        =   5
      Top             =   120
      Width           =   375
   End
   Begin VB.CommandButton cmdWriteChord 
      Caption         =   "1 key  file"
      Height          =   495
      Left            =   4080
      TabIndex        =   3
      Top             =   7320
      Width           =   615
   End
   Begin VB.CommandButton cmdClear 
      Caption         =   "&Klear"
      Height          =   495
      Left            =   6480
      TabIndex        =   2
      Top             =   7320
      Width           =   495
   End
   Begin VB.CommandButton cmdExit 
      Cancel          =   -1  'True
      Caption         =   "E&xit"
      Height          =   495
      Left            =   7080
      TabIndex        =   1
      Top             =   7320
      Width           =   495
   End
   Begin VB.Label lblDnote 
      BorderStyle     =   1  'Fixed Single
      Caption         =   "V,No"
      Height          =   255
      Left            =   7740
      TabIndex        =   50
      Top             =   3960
      Width           =   435
   End
   Begin VB.Label lblNote 
      Alignment       =   2  'Center
      BorderStyle     =   1  'Fixed Single
      Caption         =   "Note"
      Height          =   255
      Left            =   7080
      TabIndex        =   42
      Top             =   0
      Width           =   495
   End
   Begin VB.Label lblMeas 
      BorderStyle     =   1  'Fixed Single
      Caption         =   "Meas"
      Height          =   255
      Left            =   6600
      TabIndex        =   41
      Top             =   0
      Width           =   495
   End
   Begin VB.Label lblRaised 
      Alignment       =   2  'Center
      BorderStyle     =   1  'Fixed Single
      Caption         =   "^6  ^7"
      Height          =   255
      Left            =   960
      TabIndex        =   38
      Top             =   3000
      Width           =   615
   End
   Begin MSGrid.Grid grdRaised 
      Height          =   495
      Left            =   1080
      TabIndex        =   37
      Top             =   3360
      Width           =   375
      _Version        =   65536
      _ExtentX        =   661
      _ExtentY        =   873
      _StockProps     =   77
      BackColor       =   16777215
      FixedRows       =   0
      FixedCols       =   0
      ScrollBars      =   0
   End
   Begin MSGrid.Grid grdKeySig 
      Height          =   1695
      Left            =   1080
      TabIndex        =   36
      Top             =   480
      Width           =   375
      _Version        =   65536
      _ExtentX        =   661
      _ExtentY        =   2990
      _StockProps     =   77
      BackColor       =   16777215
      Rows            =   7
      FixedRows       =   0
      FixedCols       =   0
      ScrollBars      =   0
   End
   Begin VB.Label lblMyCand 
      Alignment       =   2  'Center
      BorderStyle     =   1  'Fixed Single
      Caption         =   "My"
      Height          =   255
      Left            =   6720
      TabIndex        =   33
      Top             =   3960
      Width           =   375
   End
   Begin VB.Label lblCandSolNo 
      BorderStyle     =   1  'Fixed Single
      Caption         =   "Cand Sol #"
      Height          =   255
      Left            =   120
      TabIndex        =   31
      Top             =   6360
      Width           =   855
   End
   Begin VB.Label lblGoodCands 
      Alignment       =   2  'Center
      BorderStyle     =   1  'Fixed Single
      Caption         =   "# Good"
      Height          =   255
      Left            =   4320
      TabIndex        =   25
      Top             =   3960
      Width           =   735
   End
   Begin VB.Label lblBest 
      Alignment       =   2  'Center
      BorderStyle     =   1  'Fixed Single
      Caption         =   "Best"
      Height          =   255
      Left            =   5640
      TabIndex        =   23
      Top             =   3960
      Width           =   495
   End
   Begin VB.Label lblNumCands 
      Alignment       =   2  'Center
      BorderStyle     =   1  'Fixed Single
      Caption         =   "# Candidates"
      Height          =   255
      Left            =   2520
      TabIndex        =   21
      Top             =   3960
      Width           =   1095
   End
   Begin VB.Label lblNoteNo 
      Alignment       =   2  'Center
      BorderStyle     =   1  'Fixed Single
      Caption         =   "Note No."
      Height          =   255
      Left            =   1080
      TabIndex        =   19
      Top             =   3960
      Width           =   735
   End
   Begin VB.Label lblMeter 
      Alignment       =   2  'Center
      BorderStyle     =   1  'Fixed Single
      Caption         =   "Meter"
      Height          =   255
      Left            =   120
      TabIndex        =   17
      Top             =   1800
      Width           =   615
   End
   Begin MSGrid.Grid grdExtra 
      Height          =   2790
      Left            =   1080
      TabIndex        =   16
      Top             =   4440
      Width           =   7935
      _Version        =   65536
      _ExtentX        =   13996
      _ExtentY        =   4921
      _StockProps     =   77
      BackColor       =   16777215
      Rows            =   12
      Cols            =   12
   End
   Begin MSGrid.Grid grdKey 
      Height          =   735
      Left            =   120
      TabIndex        =   10
      Top             =   480
      Width           =   855
      _Version        =   65536
      _ExtentX        =   1508
      _ExtentY        =   1296
      _StockProps     =   77
      BackColor       =   16777215
      Rows            =   3
      Cols            =   1
      FixedCols       =   0
      ScrollBars      =   0
   End
   Begin VB.Label lblKey 
      Alignment       =   2  'Center
      BorderStyle     =   1  'Fixed Single
      Caption         =   "Key"
      Height          =   255
      Left            =   120
      TabIndex        =   9
      Top             =   120
      Width           =   855
   End
   Begin VB.Label lblSong 
      Alignment       =   2  'Center
      BorderStyle     =   1  'Fixed Single
      Caption         =   "Song:"
      Height          =   255
      Left            =   1080
      TabIndex        =   4
      Top             =   120
      Width           =   615
   End
   Begin MSGrid.Grid grdTable4P 
      Height          =   3375
      Left            =   1680
      TabIndex        =   0
      Top             =   480
      Width           =   7335
      _Version        =   65536
      _ExtentX        =   12938
      _ExtentY        =   5953
      _StockProps     =   77
      BackColor       =   16777215
      Rows            =   25
      Cols            =   11
   End
End
Attribute VB_Name = "frmTable4P"
Attribute VB_Creatable = False
Attribute VB_Exposed = False
' 4PartZ.VBP was saved from 4PartY.VBP on 10aug08
' All back level subs, eg Name9, were dropped 10aug08
' from 4PartZ.VBP.

' All variables MUST be declared.
Option Explicit

' This Visual Basic 4 program, 4PartH.vbp,
' creates four-part chords and does four-part
' harmony and analysis.
' See AProcedureMap for comments and a map
' of the procedure and functions calls.

' Note that if a Const is used in a Private Type def,
' the Const must come before the Type def.

' It seems I have to hard code CANDMAX, but
' sometimes I have to enlarge it for some songs:

'Const CANDMAX As Long = 60000 ' big arbitrary no.
'Const CANDMAX As Long = 160000 ' big arbitrary no.
'Const CANDMAX As Integer = 30000 ' big arbitrary no.
Const CANDMAX As Integer = 20000 ' big arbitrary no.
'Const CANDMAX As Integer = 15000 ' big arbitrary no.
'Const CANDMAX As Integer = 12000 ' big arbitrary no.
'Const CANDMAX As Integer = 9000 ' big arbitrary no.
'Const CANDMAX As Integer = 2000 ' big arbitrary no.

' This is the only hard-coded file name.
' All the other filename are contained within it.
Const FILEONEPREFIX As String = "C:\vb4prg\4part\"
Const FILEONENAME As String = "4PartFN" & ".TXT"
Dim gFileOnePrefix As String
Dim gFileOneName As String

Const TONIC As Integer = 1
Const SUPERTONIC As Integer = 2
Const MEDIANT As Integer = 3
Const SUBDOMINANT As Integer = 4
Const DOMINANT As Integer = 5
Const SUBMEDIANT As Integer = 6
Const LEADINGTONE As Integer = 7
Const SUBTONIC As Integer = 7 ' b7^

Const SOPRANO As Integer = 1
Const ALTO As Integer = 2
Const TENOR As Integer = 3
Const BASS As Integer = 4

Const NOENTRY As Integer = -1 ' flag no input
'   entered for variables that have a valid
'   zero value.
Const NOTSET As Integer = 0 ' flag no input
'   entered for variables that have no valid
'   zero value.

' W=whole note, H=half note, Q=quarter note,
' E=eighth note, S=sixteenth note, T=thirty-second.
Const TIMENAME = _
    "WH.Q...E.......S|.||...........T"
'   "12345678901234567890123456789012"
'    00 0   0       11 1            3
'    12 4   8       67 9            2
Const SINGLEBAR As Integer = -1
Const DOUBLEBAR As Integer = -2

Const ZEROBYTE As Byte = 0
Const CONTRARY As Byte = 1
Const OBLIQUE As Byte = 2
Const Similar As Byte = 4
Const STATIONARY As Byte = 8
Const CONVERGING As Byte = 16
Const DIVERGING As Byte = 32
Const RISING As Byte = 64
Const FALLING As Byte = 128

Dim gbRankOverVariance As Boolean
Dim gbRankLE As Boolean
Dim gbVarianceLE As Boolean
Dim gVarRound As Single
Dim gxOuterMotion As Byte
Dim gxInnerMotion As Byte
Dim gNumOuterContrary As Integer
Dim gNumOuterOblique As Integer
Dim gNumOuterSimilar As Integer
Dim gNumOuterStationary As Integer
Dim gNumInnerContrary As Integer
Dim gNumInnerOblique As Integer
Dim gNumInnerSimilar As Integer
Dim gNumInnerStationary As Integer
'Dim gOuterIndex As Integer
Dim gxMotionRequest As Byte
Dim gbRaise67OneVoice As Boolean ' OneVoice/AnyVoice
Dim gbRaise67Close As Boolean ' Close/Octave
Dim gMinVarianceForDupes As Single ' discourage close V-V's

' Each record in gNote array contains the indexes,
' FirstCand and LastCand, into this array gCand,
' specifying a range of indexes to array gChord.
' Each such entry in gChord is a valid candidate
' four-part chord for this note.
Private Type CandRec
    bUsed As Boolean
    Index2Ch As Integer ' pointer to gChord array
    Variance As Single ' measure of difference
    Rank As Integer ' 0 is blank, 1 is best, 2 less
    OuterVariance As Single ' difference between sop and bass
    InnerVariance As Single ' difference between alto and ten
    xOuterMotion As Byte ' Contrary, Oblique, etc for Sop Bas
    xInnerMotion As Byte ' Contrary, Oblique, etc for Alt Ten
    OuterGraphic As String ' for Sop Bas
    InnerGraphic As String ' for Alt Ten
    'MotionDiff As Single ' measure of difference of the
    ' voices iMotionn Similar (>0) or Contrary Motion (<0)
    'PrevBest As Integer ' 0 unless NextSol
    PrevBest As Long ' 0 unless NextSol
    xHarmProgFlag As Byte ' flag special harmonic
    '   progressions
    Error As String
    'ErrorBits As Long ' save errors=powers of two bits
    Error2 As String ' Final or User
    ' between SAT's of gNotex and gAfterX,
    ' = sum of sqrs of SAT differences.
    bSmooth As Boolean ' accept any very smooth
    ' harmonic progression
    bDupe As Boolean ' same scale degree step as gAfterX,eg V-V
    VSlash As String ' SecondDominant
End Type

Const MAXVOICESPREAD As Integer = 12
'Const MAXVOICESPREAD As Integer = 18
' gCand(0) is used for re-initializing
Dim gCand(0 To CANDMAX) As CandRec
'Dim gCandLast As Integer ' index of highest used in this song
Dim gCandLast As Long ' index of highest used in this song
Dim gxAfterFlag As Byte

Const FBFIELDMAX As Integer = 4
Const ALTERMAX As Integer = 7
Const INCRSHARP As Integer = 1
Const INCRFLAT As Integer = -1

Const SLURBEG As Byte = 128 ' "("
Const SLUREND As Byte = 1   ' ")"
Const SLURALL4 As Byte = 4 ' "(*"
Const SLURNONE As Byte = 0

Const TIE As Byte = 128 ' "-"
Const TIEALL4 As Byte = 4 ' "-*"
Const TIENONE As Byte = 0

Const REST1 As Byte = 128 ' "-"
Const RESTALL4 As Byte = 4 ' "-*"
Const RESTNONE As Byte = 0

' Change this tone to a rest (Z) or invisible rest(X)
' in sub WriteABCNotes
Const WRITEZ As Byte = 128
Const WRITEX As Byte = 1
Const WRITEASIS As Byte = 0

' Used with xNHT.
' At least one of the NHTFIRST or _
' NHTHANG notes is NHTNONHARM(onic).
' A NHT group is any "full chord" note NHTFIRST
' followed by a note NHTHANG with a minus in
' lines T1: to T4: (rows 17-20 in grdTable),
' or any "hanging" note NHTHANG
' which has a minus in lines T1: to T4:.
Const NHTNONE As Integer = 0
Const NHTFIRST As Integer = 1
Const NHTHANG As Integer = 2
Const NHTHARMONIC As Integer = 4
Const NHTNONHARM As Integer = 8
Const NHTFINISHED As Integer = 128

Const LYRICLINEMAX As Integer = 4
Dim gNumLyricLines As Integer

Const TRIADALLOWEDMAX As Integer = 7
Dim gTriadAllowed(1 To TRIADALLOWEDMAX) As Boolean
' True=allow this triad no or Roman num chord
' Set by input line: OP: INCLUDE I V etc

Const FIGBASSALLOWEDMAX As Integer = 6
Dim gbFigBassAllowed(0 To FIGBASSALLOWEDMAX) As Boolean
' True=allow this figured bass
' Set by input line: OP: FIGBASS NONE 53 6 etc

Dim gbSeventhExcluded As Boolean ' 0=false allow the
' seventh degree in a chord; -1=true disallow it.
' Note early medieval mode music did not allow
' the seventh of a chord as too dissonant.

Dim gbMajorSeventhExcluded As Boolean ' 0=false or
' allow the major triad-major seventh eg I7 or IV7.
' -1-true disallow it, as a bit too dissonant.

' Used in Noterec Motion().
Const MOFF As Integer = 0
Const MCONTRARY As Integer = 1
Const MOBLIQUE As Integer = 2
Const MSIMILAR As Integer = 3
Const MSTATIONARY As Integer = 4

Private Type TimeRec ' see VoiceRec2 and NoteRec
    Name As String ' W,H,Q,E,S,T or Q>> or E< or H3/2
    Val As Integer ' T1:-T4: cards, W=1,H=2...T=32
    Units As Integer ' Tn: =WHOLENOTEUNITS/.Val
    Pos As Integer ' 1st note in measure is at pos=0
    Offset As Integer ' -n means the nth note back
    DefaultPos As Integer ' DefaultPos=.Pos is set
    ' by Tn: card for any higher Tn+1: to T4: card,
    ' as a convenience in input.
End Type

' TODO change .Index to .Index2Name
Private Type VoiceRec2 ' see VoiceRec
    Name As String ' with pre and post chars,
    ' eg =C4,C4,C#4,#C4,xC4,Ab4,bA4,bbA4
    Index2Name As Integer ' index into gNoteName array
    NameFix As String ' replaces .Name on input
    gName As String ' =gNoteName(.Index2Name) for test
    Quoted As String ' quotes prefixed to note eg "G7"!p!
    PreNote As String ' eg <.(*((.
    PostNote As String ' eg ))*-*
    ChordPos As Integer '1=root,3=3rd,5=5th of chord
    Time As TimeRec
    Effort As Integer ' mid,high,extreme range
    ' indicated by "fibonacci" numbers
    OpenSlurs As Integer ' e.g. ((*(G4
    CloseSlurs As Integer ' e.g. G4)*)
    xSlur As Byte ' SlurBeg=128,SlurAll4=4,SlurEnd=1 else=0
    xTie As Byte ' TIE=128,TieAll4=4 else=0
    xRest As Byte ' Rest=128,Restall4=4,RestNone=0
    xWriteXZ As Byte ' WriteZ=128,WriteX=1,else=0
    bNHT As Boolean ' True=non harmonic tone
    Lyric(1 To LYRICLINEMAX) As String ' field on
    ' lines W1 to W4
End Type

Private Type NoteRec
    Voice(SOPRANO To BASS) As VoiceRec2
    bUsed As Boolean
    SongKey As Integer ' x in gSong(gnotex).Key(x)...
    SongKeySet As Boolean ' if non-zero, then true,set
    TMName As String ' convenient for display
    Time As Integer ' divisor 1=W 2=H 4=Q 8=E 16=S 32=T
    MeasTime As TimeRec
    xNHT As Byte ' at least one of group is NHTNONHARM
    bNHTDelayed As Boolean ' prev note has<4 input tones
    RomNum As Integer ' scale degree of the root
    RNName As String ' convenient for display
    ' TODO not needed
    'FigBass(1 To FBFIELDMAX) As Integer
    'NumFB As Integer ' highest used index in FigBass
    FBName As String ' convenient for display
    ERName As String ' convenient for display
    Lyric(1 To LYRICLINEMAX) As String ' field on
    ' lines W1 to W4
    AlterAboveBass(1 To ALTERMAX) As Integer ' raise
    ' the notes at this interval above the bass by
    ' half step up (+1) or down (-1).  Set in StoreFB.
    bAlterAboveBass As Boolean ' true if
    ' AlterAboveBass(x) used
    AlterAboveBassNM As String ' concat alter fieldnames
    AlterSDeg(TONIC To LEADINGTONE) As Integer  ' raise
    ' the notes at this scale degree ^1 to ^7 by half
    ' step up (+1) or down (-1).  ^1 is Tonic, ^7 is LT.
    ' Set in sub StoreFB.
    bAlterSDeg As Boolean ' true if AlterSDeg(x) used
    AlterSDegNM As String ' concat alter fieldnames
    AlteredQuality As Integer ' 1=maj,2=min,3=dim,4=aug
    Inversion As Integer ' 0, 1, 2, 3
    ThirdIncr As Integer ' b=-1, #=1
    bSeventh As Boolean ' True=seventh chord
    Quality As Integer ' 1=maj,2=min,3=dim,4=augm
    ' Each note is a single bar or double bar or
    ' one of the next three chords:
    bBar As Boolean ' True=Singlebar or Doublebar
    bChord As Boolean ' 0=False, -1=True not bar or rest
    bRestAll4 As Boolean ' 0=False, -1=True for 4 voices
    bRests3 As Boolean ' 0=False, -1=True for 3 voices
    bFirstChord As Boolean ' 0=False, -1=True
    bPrevChord As Boolean ' 0=False, -1=True
    bFinalChord As Boolean ' 0=False, -1=True
    ' Final cadence allows 3 roots and a 3rd.
    bUserChoice As Boolean ' 0=False, -1=True
    'BeforeX As Integer ' index of valid note
    ' just before this one. See cmdHarmony_Click
    NoteX As Integer ' =gNoteX for the gAfterX note
    AfterX As Integer ' =gAfterX for the gNoteX note
    MeasureNo As Integer ' = # bars + 1
    Beat As Integer ' beat in this measure
    ChordInput As Integer ' 4 voices input for this chord
    'InputCand As Integer ' points to ChordInput > 0
    InputCand As Long ' points to ChordInput > 0
    'FirstCand As Integer ' index into gCand array
    FirstCand As Long ' index into gCand array
    'LastCand As Integer ' last index used for any
    LastCand As Long ' last index used for any
    '   candidate chord
    'BestCand As Integer ' index into gCand array
    BestCand As Long ' index into gCand array
    'BestCandIndex As Integer ' reset back to BestCand
    BestCandIndex As Long ' reset back to BestCand
    'UserCand As Integer ' chosen by user in MyCand box
    UserCand As Long ' chosen by user in MyCand box
    'nGoodCand As Integer ' number of good cands
    nGoodCand As Long ' number of good cands
    ' without Err messages
    SolutionNo As Integer ' # of this
    '   solution for this note gNoteX.
    SolutionNoSave As Integer ' to restore the song
    NumContraryOblique As Integer
    bMustAllowError As Boolean ' -1=true allow the
    ' error option e.g. X7th for this note since
    ' backtracking up to n=3 notes got no solution.
    bSeventhExcluded As Boolean ' -1=true = exclude
    ' the 7th of a chord e.g. the F in G B D F.
    bTriadAllowed(1 To TRIADALLOWEDMAX) As Boolean
    ' True=allow this triad no or Roman num chord
    'bFigBassAllowed(0 To FIGBASSALLOWEDMAX) As Boolean
    ' True=allow this figured bass 7may08
    bCrossedVoices As Boolean ' 0=false, else true
    bCrossedBass As Boolean ' 0=false, else true 3aug08
    'CrossedDisplay(SOPRANO To BASS) As Integer ' index v
    ' into gnote(gnotex).voice(v).name to display as crossed.
    bNewRaiseHS As Boolean ' new this note
    RaiseHS As Integer ' raise input note by halfsteps
    
End Type

Dim gNoteCurr As Integer ' current index to gNote
' usually the same as gNoteX.
Dim gNoteLast As Integer ' index of last/highest used

'Const SONGNOTEMAX As Integer = 300
Const SONGNOTEMAX As Integer = 2000
' Define gNote(0) as insurance. First note is 1.
' gNote(0) is used for re-initializing
Dim gNote(0 To SONGNOTEMAX) As NoteRec
'Dim gMySolution As Integer ' user override

' NOTE - I moved .BestCand from gHarm().BestCand
'       to gNote().BestCand.  This note below
' still applies.
' Array gHarm mirrors the notes in array gNote.
' The .FirstNote and .LastNote indexes in gSong
' apply to both gHarm and gNote.
' gNote(x).FirstCand points to the first candidate,
' gNote(x).LastCand points to the last valid
' four-part candidate in array gCand for note x.
' gHarm(x).BestCand points to the best candidate
' in array gCand out of that range.
' gCand(y).Index2Ch points to a four-part
' chord in gChord.

' Array gHarm contains the chords selected from
' array gChord which are the four-part harmonized
' versions of the notes input in gNotes.
'Dim gHarmLast As Integer ' index of last/highest used
'Const HARMNOTEMAX As Integer = 100

' TODO move BestCand to gNote array

' Following 31 powers of two are used in
' sub StoreOptionAllow, but the const names aren't.
    ' Note an error name cannot match the last
    ' characters of another error name,
    ' e.g. cannot use Inv because it matches 2Inv.
Const P5 As Integer = 1
Const P8 As Integer = P5 * 2
Const HID5 As Integer = P8 * 2
Const HID8 As Integer = HID5 * 2
Const PROGNV As Integer = HID8 * 2 ' must come before PROG
Const PROG63 As Integer = PROGNV * 2
Const PRGANY As Integer = PROG63 * 2
Const PRGBAS As Integer = PRGANY * 2
Const PRGEXT As Integer = PRGBAS * 2
Const PRG6TH As Integer = PRGEXT * 2
Const PRG7TH As Integer = PRG6TH * 2
Const NOTFIN As Integer = PRG7TH * 2
Const SIM4 As Integer = NOTFIN * 2
Const BXT As Integer = SIM4 * 2
Const TXA As Long = BXT * 2
Const AXS As Long = TXA * 2
Const INVR As Long = AXS * 2
Const NOT7TH As Long = INVR * 2
Const USER As Long = NOT7TH * 2
Const INV2 As Long = USER * 2
Const X7TH As Long = INV2 * 2 ' exclude 7th chords
Const BRANGE As Long = X7TH * 2  ' Best Range
Const ABOVE As Long = BRANGE * 2  ' alter note above bass
Const BASSGAPP As Long = ABOVE * 2  ' T-B<=.BassGap
Const TIE4 As Long = BASSGAPP * 2  ' force same chord
Const REST As Long = TIE4 * 2  ' no rests around 6 4
Const DUPE As Long = REST * 2  ' allow dupe of gAfterX
Const INCRM As Long = DUPE * 2 ' allow increm err #^3
Const P4INT As Long = INCRM * 2 ' allow interval > P4 for A T
Const FBOPT As Long = P4INT * 2 ' allow FB types
Const TRIPLE As Long = FBOPT * 2 ' triple root
' ONLY 32 BITS FIT IN A LONG INTEGER
'Const PAIRS As Long = TRIPLE * 2 ' two pairs of note names

Const ERRNAMEMAX As Integer = 31
Dim gErrNameLC As Variant ' not used yet; good as doc?
Dim gErrName As Variant ' init in sub Init4Part
Dim gAllowErrors$ ' allowed on Pass 4
Dim gAllowErrorsPass1$ ' allowed on Pass 1
Dim gErrorNames$

' a LONG integer has 32 bits
Dim gxErrorAllowed As Long ' If a bit is set,
' then allow that error number for a candidate.
' Set by input line: OP: ALLOW Sim4 (tab) Hid5 etc
Dim gxErrorAllowedPass1 As Long ' If a bit is set,
' then allow that error number for a candidate in Pass1.
' Set by input line: OP: ALLOW PASS1 Sim4 (tab) Hid5 etc
Dim gxCandErrorBits As Long ' all the error bits
' set for a candidate chord by fcnbCandChord
' calling fcnErrallow.

' Used in fcnErrAllow
Dim gLowestErrNum As Integer
Dim gNumCandErrs As Integer
Dim gSingleAllowedErrNum As Integer
Dim gLowestAllowedErrNum As Integer
Dim gNumAllowedCandErrs As Integer
Dim gbNewCand As Boolean

' used in Harmonize, fcnbNextBest and fcnErrAllow
Dim gbErrorFourthPass As Boolean ' -1 = true, set
' in sub Harmonize and used in fcnErrAllow.
Dim gMaxNextBestNote As Integer ' see fcnbNextBest
Const MAXNEXTBEST As Integer = 2 ' default
Dim gMaxNextBest As Integer ' 1 to 4, default=2.
' 1 is probably not useful; 2 seems to work well;
' 3 is reasonably fast; 4 may be 100000 loops.
Const BASSGAPMAX As Integer = 36
Const BASSGAPDEFAULT As Integer = 24
Dim gBassGap As Integer

Const BARSMAX As Integer = 300 ' some large no.
Dim gBarBeg As Integer
Dim gBarEnd As Integer
Dim gBarsVoice(SOPRANO To BASS) As Integer
Dim gBarsLyric(SOPRANO To BASS) As Integer

Dim gbSmoothOption As Boolean ' OP: MISC SMOOTH ON/OFF
Dim gbSmooth As Boolean '  if True, then
' ignore the PgmTbl harmonic progression rules
' since no voice moves more than a whole
' step between gNoteX and gAfterX. Used in
' fcnbCandChord.  Ottman p 287, Bach examples

' If gbAlto3 then output data for the Alto
' voice for printing by abcm2ps in an
' individual part Alto clef.
Dim gbAlto3 As Boolean ' proposed OP: MISC ALTO3 ON

'Dim gbDisplayHarmony As Boolean ' False=display
' the notes input from the array gNote.
' True=display the generated harmonized
' four-part notes from the best candidates
' pointed to by gNote().BestCand.
' Toggle with the cmdDisplayHarmony_Click button.

' Set in sub StoreOptionRange. Used in ScanTriads
' under the names gS1, etc.
' Cannot use an ordinary string variable in a
' Type definition.

' Note a fixed string will always stay a fixed
' string no matter what it is copied to, and trim
' not work on it.  You must convert a fixed string
' to an ordinary string variable (of varying
' length) in order to use trim on it.

' You cannot compare a fixed string of all
' blanks with a variable string of all blanks.
' You have to define an empty sting of all blanks
' to do the comparison.
' See sub StoreOptionRange.
' See sub InitVoiceRange.

Const TM As Integer = 1
Const T1 As Integer = 2
Const T2 As Integer = 3
Const T3 As Integer = 4
Const T4 As Integer = 5
Const V1 As Integer = 6
Const V2 As Integer = 7
Const V3 As Integer = 8
Const V4 As Integer = 9
Const RN As Integer = 10
Const FB As Integer = 11
Const W1 As Integer = 12
Const W2 As Integer = 13
Const W3 As Integer = 14
Const W4 As Integer = 15
Const W11 As Integer = 16
Const W12 As Integer = 17
Const W13 As Integer = 18
Const W14 As Integer = 19
Const L11 As Integer = 20
Const L21 As Integer = 21
Const L31 As Integer = 22
Const L41 As Integer = 23
Const ER As Integer = 24
Const HS As Integer = 25
Const XX As Integer = 26
Const ZZ As Integer = 27
Const LASTUSEDMAX As Integer = ZZ

' Type names on 4Part format cards, e.g. TM:, V3:
' The string can be searched fast by INSTR.
Const LASTNAME1 As String = "01TM 02T1 03T2 04T3 05T4 "
Const LASTNAME2 As String = "06V1 07V2 08V3 09V4 "
Const LASTNAME3 As String = "10RN 11FB "
Const LASTNAME4 As String = "12W1 13W2 14W3 15W4 "
Const LASTNAME5 As String = "16I1 1611 1712 1813 1914 "
Const LASTNAME6 As String = "201I 2011 2121 2231 2341 "
'Const LASTNAME6 As String = "201I 212I 223I 234I "
Const LASTNAME7 As String = "24ER 25HS"
Const LASTNAMES As String = LASTNAME1 & LASTNAME2 _
    & LASTNAME3 & LASTNAME4 & LASTNAME5 _
    & LASTNAME6 & LASTNAME7

Const WHOLENOTEUNITS As Integer = 32
Dim gNotePos(TM To T4) As Integer

Const NUMKEYMAX As Integer = 5
Const SONGKEYMAX As Integer = NUMKEYMAX

' See MainChordsForKey and fcnbCandChord
Private Type KeyChordsRec
    FirstChord(1 To 7) As Integer ' index of the
    ' first chord in gChord of this scale degree.
    FinalChord(1 To 7) As Integer ' index of  the
    ' last chord in gChord of this scale degree.
End Type
Dim gKeyChords(1 To NUMKEYMAX) As KeyChordsRec

' TODO get rid of the fixed strings by putting
' the fields in the body of the type def.
Private Type RangeRec2
    Bot As String * 3
    Top As String * 3
    BestBot As String * 3 ' easy, good sounding range
    BestTop As String * 3 ' easy, good sounding range
End Type

Dim gRangeDefault(SOPRANO To BASS) As RangeRec2
 

' In sub StoreNotes, store the main song key, K1,
' and any modulation keys, K2 to K4.
' Each note in gNote(x) points to one of the keys.
Private Type KeyRec
    Name As String * 2 ' G#, Bb etc
    Mode As Integer ' 1=MAJOR, 2=MINORNAT,...
End Type

' E.g. M:4/4, L:1/8 @WN=32U, U=1/32nd whole note.
' W=32U,H=16U,Q=8U,E=4U,S=2U,T=1U TimeUnits.
' LU=32U/8 = 4U TimeUnits per default 1/8 note.
' MU=32U*4/4 = 32U TimeUnits per measure.

' E.g. M:4/4, L:1/8 @WN=48U, U=1/48 for triplet=R.
' W=48U,H=24U,Q=12U,E=6U,R=4U,S=3U,T=1.5U TimeUnits.
' 3 1/8 note triplets = 3*R = 3*4U = 12U has the time
' of 2 1/8 notes = 2E = 2*6U = 12U, so 1 triplet=R=4U.
Private Type MSigRec ' see VoiceRec2 and NoteRec
    Text As String ' 4/4 for M:4/4
    Numer As Integer ' top no=n if M:n/d
    Denom As Integer ' bot no=d if M:n/d
    MeasUnits As Integer ' If M:n/d, then MeasUnits =
    ' M/U = WN*n/d U's. Eg if M:4/4, then
    ' M/U = 32*4/4 U's per measure.
    BeatUnits As Integer ' .L.WNUnits/.M.Denom eg 32/4
End Type

Dim gWNUnitsText As String ' "@WN=32U" WN=Whole Note
Const DEFAULTTIMES = _
"W 1/1 H 1/2 Q 1/4 E 1/8 S 1/16 T 1/32 "
Private Type LSigRec ' see VoiceRec2 and NoteRec
    Text As String ' eg. 1/8 for L:1/8 @WN=32U
    UText As String ' is  @WN=32U if L:1/8 @WN=32U
    Default As String ' E if L: 1/8
    Numer As Integer ' top no=1
    Denom As Integer ' bot no=d if L:1/d
    WNUnits As Integer ' Whole Note WN = 32U
    ' All time positions and durations in a
    ' measure are calculated in integer U units.
    LUnits As Integer ' LUnits = WN/d = 32U/d = 4U
    TimeUnit As Single ' If WN=32U, then
    ' TimeUnit = U = 1/32nd whole note.
End Type

Private Type SongRec
    bUsed As Boolean
    X As Integer ' input song number: 1,2,3...
    T As String * 50 ' text description of song
    M As MSigRec ' time signature eg. M:4/4
    L As LSigRec ' default note time eg L:1/8 @WN=32U
    ' where "=4U" is needed only on first use.
    QText As String
    NumKey As Integer
    Key(1 To NUMKEYMAX) As KeyRec
    'KY(1 To 3) As String ' key, MAJOR or MINOR,
    ' NATURAL or HARMONIC or MELODIC (minor).
    'KeyMode As Integer ' 1=MAJOR, 2=MINORNAT,...
    KeySigUsedIst As Integer
    KeySigIst(1 To 7) As String ' first key sig in song
    KeySigUsed As Integer
    KeySig(1 To 7) As String ' curr key sig in song
    ' may be first key sig or modulation key sig.
    bTriadAllowed(1 To TRIADALLOWEDMAX) As Boolean
    ' True=allow this triad no or Roman num chord
    bFigBassAllowed(0 To FIGBASSALLOWEDMAX) As Boolean
    xErrorAllowed As Long ' If bit 2^n is set,
    ' then allow that error number in Pass 4
    xErrorAllowedPass1 As Long ' If bit 2^n is set,
    ' then allow that error number in Pass 1
    'RangeType As Integer ' InitVoiceRange
    RANGE(SOPRANO To BASS) As RangeRec2
    RangeTop(SOPRANO To BASS) As Integer
    RangeBot(SOPRANO To BASS) As Integer
    RangeBestTop(SOPRANO To BASS) As Integer
    RangeBestBot(SOPRANO To BASS) As Integer
    NumSingleBars As Integer ' single measure bars
    NumDoubleBars As Integer ' double measure bars
    FirstNote As Integer ' index into gNote array
    LastNote As Integer ' last index used for
    ' notes in gNote arrays.
    ' LastV1-LastFB are offsets from FirstNote.
    LastNoteTM As Integer ' last index used for TM
    ' TODO not used:
    'LastField As Integer ' index of last field
    ' input, usually"||".
    LastUsed(1 To LASTUSEDMAX) As Integer ' last note
    ' used for a card of this type: FB, RN etc
    ' leave for testing
    BassGap As Integer ' Tenor - Bass <= .BassGap
    xMotionRequest As Byte ' CONTRARY or OBLIQUE, etc
End Type

Const SONGMAX As Integer = 50
' Define gSong(0) as insurance. First song is 1.
' gSong(0) is used for re-initializing
Dim gSong(0 To SONGMAX) As SongRec
Dim gSongLast As Integer ' index of last/highest used
Dim gSongCurr As Integer ' current song to display
Dim gSongNext As Integer ' next song to display, used
' only by cmdNextSong_Click.
Dim gbFirstSongBegun As Boolean ' If True, X: was input,
' (If False, E: was input (End of song).)

Dim gPrefix As String
Dim gDirSongs As String
Dim gDirChords As String
Dim gAbcFile As String
Dim g4PartFile As String
Dim gFile As String

Dim gMainVoice As Integer ' OP: MISC MAINVOICE Soprano etc
Dim gInputLineNo As Integer
Dim gInputErrs As Integer
Dim gbRaised As Boolean
Dim gRaiseInput(1 To 4) As Integer
' Used in subs InputLine, GetLine, PushBackLine
Const MAXINPUTSTACK As Integer = 15
Dim gInputStackNo As Integer
Dim gInputStack(MAXINPUTSTACK) As String

Dim gSongX As Integer ' current index to gSong,
' same as gSongCurr.
Dim gNoteX As Integer ' current index to gNote,
'  sometimes the same as gNoteCurr.
Dim gBeforeX As Integer ' note just before gnotex
Dim gAfterX As Integer ' note just after gnotex

Dim gRomNum As Integer ' current scale deg or 0.
Dim gChordX As Integer ' current index to gChord
Dim gScaleDegNeeded As Integer ' for next note
Dim gInversionNeeded As Integer ' for next note
Dim gbRomNumSave(1 To TRIADALLOWEDMAX) As Boolean
Dim gRomNumDegreeSave As Integer
Dim gAlteredChord As Integer ' =6 if melup, =7 if harm.

'Const FIELDMAX As Integer = 24 ' big arbitrary no.
'Const FIELDMAX As Integer = 500 ' big arbit no,for Bach songs?
Const FIELDMAX As Integer = 2000 ' big arbit no,for Bach songs?

Dim gCopyControlsVoice As Integer ' VOICE1 to VOICE4
Dim gCommentVoices As String ' input V:n to comment out
Dim gCommentLyrics As String ' input I:n to comment out
Dim gCommentTimes As String ' input Tn: to comment out

Dim gFieldLast As Integer ' highest index used
Dim gField(FIELDMAX) As String ' gTextLine fields
Dim gTextLine As String ' input line
Dim gTextLineFull As String ' input line including % comment

Dim gbSharpKey As Boolean ' sharp=true,flat=false
Const HALFSTEPNAMEMAX As Integer = 12 ' 14aug08
Dim gHalfStepName0(0 To HALFSTEPNAMEMAX) As String
Dim gHalfStepName(0 To HALFSTEPNAMEMAX) As String
Dim gHalfStepNameNatural(0 To HALFSTEPNAMEMAX) As String

' Types of scales
Const MAJOR As Integer = 1
Const MINORNAT As Integer = 2
Const MINORHARM As Integer = 3
Const MINORMELUP As Integer = 4
Const MINORMELDN As Integer = 5
' The medieval modes uses only the white keys
' but each starts on a different pitch of C Major.
Const DORIAN As Integer = 6
    ' Dorian starts on D. Like natural minor
    ' with a raised sixth scale step
Const PHRYGIAN As Integer = 7
    ' Phrygian starts on E. Like natural minor
    ' with a lowered second scale step
Const LYDIAN As Integer = 8
    ' Lydian starts on F. Like major
    ' with a raised fourth scale step
Const MIXOLYDIAN As Integer = 9
    ' Mixolydian starts on G. Like major
    ' with a lowered seventh scale step
Const AEOLIAN As Integer = MINORNAT ' starts on A
Const IONIAN As Integer = MAJOR ' starts on C
' LOCRIAN, the theoretical mode starting on B,
' was not considered practical or useful because
' its tonic to dominant interval is the tritone.

'Const MINOR As Integer = 10 ' generic minor

' See InitScales
Private Type ScaleRec
    RNumName(1 To 7) As String ' I,ii,iii,IV,V,vi,vii o6
    Qual(1 To 7) As Integer ' 1=QUALMAJOR, 2=QUALMINOR,...
    HalfStep(1 To 7) As Integer ' 0,2,4,5,7,9,11
    FirstChord(1 To 7) As Integer ' index of the
    ' first chord in gChord of this scale degree.
    FinalChord(1 To 7) As Integer ' index of  the
    ' last chord in gChord of this scale degree.
End Type
Dim gbScalesSetup As Boolean
Const SCALESMAX As Integer = MIXOLYDIAN ' 9
Dim gScale(1 To SCALESMAX) As ScaleRec
Dim gScaleDeg As Integer ' 1-7 for 1^-7^ = I-vii o6

 ' Intervals above the TONIC in 12 halfstep scale
Const PERFECTOCTAVE As Integer = 12
Const MAJORSEVENTH As Integer = 11
Const MINORSEVENTH As Integer = 10
Const MAJORSIXTH As Integer = 9
Const MINORSIXTH As Integer = 8
Const PERFECTFIFTH As Integer = 7
Const TRITONE As Integer = 6
Const PERFECTFOURTH As Integer = 5
Const MAJORTHIRD As Integer = 4
Const MINORTHIRD As Integer = 3
Const MAJORSECOND As Integer = 2
Const MINORSECOND As Integer = 1

' Table of harmonic pregressions, Ottman p 206-8
' E.g. a tonic chord can progress to any of
' the scale degrees x (from 1 to 7) in
'   MajorProgress(TONIC).bScaleDeg(x)
' where .bScaleDeg(0) contains the count of
' valid scale degrees in this row of the table.
Const PROGRESSIONMAX As Integer = 12
' Should change ScaleDeg to bScaleDeg in ProgressRec
' but not in ChordRec.  7may08
Private Type ProgressRec
    bScaleDeg(1 To PROGRESSIONMAX) As Boolean
    FromInversion(1 To PROGRESSIONMAX) As Integer
    ' 0=none needed,1=rootpos,2=1st inv,4=2nd inv,
    ' 8=3rd inversion needed.
    'Common(1 To PROGRESSIONMAX) As Integer ' 0=common,
    '   1=uncommon, 2=rare
End Type
' index 8 for ii 6, index 9 for vi-iii-IV
Dim gMajProgression(TONIC To LEADINGTONE) _
    As ProgressRec

Dim gMinNatProg(TONIC To LEADINGTONE) _
    As ProgressRec

Dim gMinHarmProg(TONIC To LEADINGTONE) _
    As ProgressRec

Dim gMinMelUpProg(TONIC To LEADINGTONE) _
    As ProgressRec

Dim gBassEffort As Variant ' initialized by an array.
Dim gTenorEffort As Variant ' initialized by an array.
Dim gAltoEffort As Variant ' initialized by an array.
Dim gSopranoEffort As Variant ' initialized by an array.
Const EFFORTMAX As Integer = 26 ' set to the
'   largest "Max" in InitVoiceEffort.
Dim gbEffortSetup As Boolean ' True=gEffort() filled
Dim gEffort(SOPRANO To BASS, EFFORTMAX) As Integer

' Although the lowest human voice bass note
' is taken as G1, I need to fill the notes
' low enough in sub InitTriads.
' Initialize gNoteName array with string names
' of the notes from C0,C#0,D0,...,C8
' where C4 is middle C.  C0 is in gNoteName(0)
' and C8 is in gNoteName(NOTESMAX=96).
' Dependent on gKeyName.
' NOTICE - In order to allow for A0 as the lowest
' note of the piano's 88 keys, I start the table
' with C0 to B0, followed by C1 to B1, where C1 is
' the lowest C on the piano.

Const NUMOCTAVES As Integer = 7 ' See InitNoteNames
'Const NOTESMIN As Integer = -1 ' index of Cb0=B(-1)
Const NOTESMIN As Integer = 0
Const TRIADSMIN As Integer = -3 ' A(-1),A#(-1),B(-1),C0
Const NOTESMAX As Integer = (1 + NUMOCTAVES) * 12 ' 96
Dim gNoteName(NOTESMIN To NOTESMAX) As String ' eg "G#0"
' Used for notes in OP: MISC RANGE F2-G4 etc:
Dim gNoteNameCMajor(NOTESMIN To NOTESMAX) As String

Const Root As Integer = 1 ' note of triad
Const FIRST As Integer = 1 ' note of triad
Const THIRD As Integer = 3 ' note of triad
Const FIFTH As Integer = 5 ' note of triad
Const SEVENTH As Integer = 7 ' note of 7th chord
'Dim gTriads(TRIADSMIN To NOTESMAX) As Integer ' e.g.1=G,3=B,5=D
' gTriads() runs from C0 ... G0 A0 ... C8, where
' A0 is the lowest key on the piano keyboard and
' C8 is the highest key on the piano keyboard.
Dim gTriads(NOTESMIN To NOTESMAX) As Integer ' e.g.1=G,3=B,5=D

Dim gPrimaryTones(NOTESMIN To NOTESMAX) As Integer
    ' 1=1^,4=4^,5=5^
Dim gNeverDouble(NOTESMIN To NOTESMAX) As Integer
    ' 7=7^,6=#6^,7=#7^
Dim gScaleDegree(NOTESMIN To NOTESMAX) As Integer
Dim gNumOfTriadNotes(1 To 7) As Integer ' used to count the
' number of, e.g., G's, B's, D's in a GBD chord.
Dim gTriadNoteIndex(1 To 7) As Integer ' used to check for
' doubled altered notes in a chord: #^6 or #^7.

Dim gbDebugPrint As Boolean ' print lines if True
Dim gbDebugPrintSave As Boolean ' save it's setting
Dim gbDebug As Boolean ' used in Harmonize
Dim gbfcnToIxErrMsg As Boolean ' used in fcnToIx

' Ranges of SATB voices, set in InitVoiceRange.
' Used in proc ScanTriads.
Dim gS1 As Integer
Dim gS2 As Integer
Dim gA1 As Integer
Dim gA2 As Integer
Dim gT1 As Integer
Dim gT2 As Integer
Dim gB1 As Integer
Dim gB2 As Integer

' Best range for voice as easy or sounding better.
' Set in sub InitVoiceRange
Dim gBestS1 As Integer
Dim gBestS2 As Integer
Dim gBestA1 As Integer
Dim gBestA2 As Integer
Dim gBestT1 As Integer
Dim gBestT2 As Integer
Dim gBestB1 As Integer
Dim gBestB2 As Integer

' The integer voice range values are also stored in
' gRange(x).Top and gRange(x).Bot for convenience.
Private Type RangeRec
    Top As Integer
    Bot As Integer
End Type
Dim gRange(SOPRANO To BASS) As RangeRec

' Next FOUR are used to set scale degrees in
' gPrimaryTones and gNeverDouble arrays.
Dim gKeyName As String ' e.g. "G#"
Dim gKeyNamePrev As String ' e.g. "G#"
Dim gKeyNote As String ' e.g. "G#" & "0"
Dim gKeyIndex As Integer ' Index of "G#0" in gNoteName
Dim gKeyMode As Integer ' MAJOR, MINORNAT,...
Dim gKeyModePrev As Integer ' MAJOR, MINORNAT,...
Dim gRomNumName As String ' name of the chord based
' on this scale degree of the key: "I" to "vii o"
Dim gRomNumNameSave As String ' like gRomNumName
' used in sub fcnMinorProgression
Dim gKeySigLast As Integer '  no. in key sig
Dim gKeySig(1 To 7) As String ' sharps or flats in key sig
Dim gKeySigName(1 To 7) As String ' sharps or flats in key sig
Dim gSongKey As Integer ' index to gSong.Key(x)...
' which changes if a modulation key is entered.
Dim gSongKeyVn As Integer ' index to gSong.Key(x)...
' used only by sub StoreVoiceVn

' I changed QUALHALFDIM back to just QUALDIM 10/21/06.
Const QUALMAJOR As Integer = 1
Const QUALMINOR As Integer = 2
Const QUALDIM As Integer = 3
Const QUALAUGM As Integer = 4
Const QUALDIMDIM As Integer = 8 ' See DIMDIM7
Dim gQuality As Integer ' 1=Major,2=Minor,3=Dim,4=Augm

Const NOINVERSIONNEEDED As Integer = -1 ' flag no input
'   entered for inversion, which has a valid
'   zero value. Used for gNote(gNoteX).Inversion.
Const ROOTPOS As Integer = 0
Const FIRSTINV As Integer = 1
Const SECONDINV As Integer = 2
Const THIRDINV As Integer = 3
Dim gInversion As Integer ' 0=root,1=Ist inv,2=2nd
Dim gProgCol As Integer ' for testing

'Dim gRootIndex As Integer ' index of the root note
Dim gDoubled As Integer ' chord number 1/3/5 of the
' doubled note or the (final cadence) tripled root=1.
Dim gDoubledIndex As Integer ' index of doubled note

Dim gbRootDbl As Boolean ' 0=False, -1=True
Dim gbThirdDbl As Boolean ' 0=False, -1=True
Dim gbFifthDbl As Boolean ' 0=False, -1=True
Dim gbTonicTriple As Boolean ' 0=False, -1=True
Dim gbPrimaryTone As Boolean ' 0=False, -1=True
Dim gbSopranoDbl As Boolean ' 0=False, -1=True
Dim gbDimThirdDbl As Boolean ' 0=False, -1=True
Dim gbDimFifthDbl As Boolean ' 0=False, -1=True
Dim gbMajor7th As Boolean ' 0=False, -1=True
Dim gbDominant7th As Boolean ' 0=False, -1=True
Dim gbHalfDim7th As Boolean ' 0=False, -1=True
Dim gbDimDim7th As Boolean ' 0=False, -1=True

Dim gbSeventh As Boolean ' 0=False, -1=True
Dim gb7thComplete As Boolean ' 0=False, -1=True
Dim gbTripleRoot As Boolean ' 0=False, -1=True
Dim gbTwoPairs As Boolean ' 0=False, -1=True

' Constants for gChord.Interval(x)
Const TMINUSB As Integer = 1 ' =T-B
Const AMINUSB As Integer = 2
Const SMINUSB As Integer = 3
Const AMINUST As Integer = 4
Const SMINUST As Integer = 5
Const SMINUSA As Integer = 6 ' =S-A

' Bits set and tested in gChord(x).xHarmProgFlag
' and in gCand(x).xHarmProgFlag.
Const HARMPROG_6_3_4 As Integer = 1 ' 2^0
Const HARMPROG_I_vi6 As Integer = 2 ' 2^1
Const HARMPROG_V_iii6 As Integer = 2 ^ 2
Const HARMPROG_V_I As Integer = 2 ^ 3 ' prepare cadential
Const HARMPROG_I64_V_I As Integer = 2 ^ 4 ' cadential
Const HARMPROG_6_4_Pedal As Integer = 2 ^ 5
Const HARMPROG_6_4_Passing As Integer = 2 ^ 6
Const HARMPROG_6_4_RisingBass As Integer = 2 ^ 7
Const ALLZERO As Integer = 0

' Not used:
Const BITZERO As Integer = 1    ' 2^0
Const BITONE As Integer = 2     ' 2^1
Const BITTWO As Integer = 4     ' 2^2

Dim gChordMode As Integer ' same as gKeyMode but
' for this chord: 1=MAJOR,2=MINORNAT,3=MINORHARM
Private Type VoiceRec ' see VoiceRec2
    Index2Na As Integer ' index into gNoteName array
    ChordPos As Integer '1=root,3=3rd,5=5th,
    ' 7=7th of chord
    RaisedDegree As Integer ' 6=#^6, 7=#^7, else 0
    Effort As Integer ' mid,high,extreme range
    ' indicated by fibonnacci numbers
End Type

' TODO add links in Prev and Next(?)
    'Prev As Integer ' index of previous ChordRec or 0.
    'Next As Integer ' index of next ChordRec or 0.

Private Type ChordRec
    Voice(SOPRANO To BASS) As VoiceRec
    bUsed As Boolean
    ScaleDeg As Integer ' equals Roman numeral
    ChordMode As Integer ' 1=MAJOR, 2=MINORNAT,
    ' 3=MINORHARM, 4=MINORMELUP, same as gKeyMode
    ' set for each chord.
    AlteredChord As Integer ' =6 for #^6, =7 for #^7
    xHarmProgFlag As Byte ' flag special harmonic
    '   progressions
    RootName As String
    KeyIndex As Integer   ' index of the root
    ' TODO .InBass probably not needed, can
    '       use .Inversion
    InBass As Integer   ' 1=root,3=3rd,5=5th of chord
    Doubled As Integer ' 1=root,3=3rd,5=5th of chord
    bDupePitch As Boolean ' 0=False, -1=True
    DblType As Integer ' 1=dbl root, 2=...
    Inversion As Integer ' 0=root,1=1st,2=2nd,3=3rd
    FBName As String ' get from .bSeventh and
    ' .Inversion
    RomNumName As String ' 1-7 scale degree of
        ' root of chord
    Quality As Integer ' 1=Major,2=Minor,3=Dim,4=Aug
    bPrimaryTone As Boolean ' 0=False, -1=True
    bSeventh As Boolean ' 0=False, -1=True
    b7thComplete As Boolean ' 0=False, -1=True
    bTonicTriple As Boolean ' 0=False, -1=True
    ' Final cadence allows 3 roots and a 3rd.
    bTripleRoot As Boolean ' 0=False, -1=True
    bTwoPairs As Boolean ' 0=False, -1=True
    Interval(1 To 6) As Integer ' T-B,A-B,S-B,
    ' A-T,S-T, S-A.
    BassGapCh As Integer ' =Tenor - Bass
    bBassClose As Boolean ' 0=no=open,-1=yes=close
    ' if tenor - bass < octave, then close.
    bTriadClose As Boolean ' 0=no=open,-1=yes=close
    ' if soprano - tenor < octave, then close.
    TriadMean As Single ' TM=average of indexes of
                        ' Tenor, Alto, Soprano
    TriadStdDev As Single ' TSD=standard deviation
    ' of the indexes of Tenor, Alto, Soprano
    ' TSD=SQRT( (T-TM)^2 + (A-TM)^2 + (S-TM)^2 )
    ChordMean As Single ' CM=average of indexes of
                        ' Bas, Tenor, Alto, Soprano
    ChordStdDev As Single ' CSD=standard deviation
    ' of the indexes of Bass, Tenor, Alto, Soprano
    ' TSD=SQRT( (B-TM)^2 + (T-TM)^2 + (A-TM)^2 + (S-TM)^2 )
End Type

' Assignment to ChordRec variable must occur
' in a proc:
'   gChord(100).voice(Bass).Index=num1 ' Assign a value.

Const RANGENORMAL As Integer = 1 ' 2^0 all normal
Const RANGELOWER_SOPRANO As Integer = 2 ' 2^1
Const RANGELOWER_ALTO As Integer = 4 ' 2^2
Const RANGELOWER_TENOR As Integer = 8 ' 2^3
Const RANGELOWER_BASS As Integer = 16 ' 2^4
Const RANGEHIGHER_SOPR As Integer = 32 ' 2^5
Const RANGEHIGHER_ALTO As Integer = 64 ' 2^6
Const RANGEHIGHER_TENOR As Integer = 128 ' 2^7
Const RANGEHIGHER_BASS As Integer = 256 ' 2^8
Const RANGEMAX As Integer = 512 ' 2^9 all max
Dim gRangeType As Integer ' InitVoiceRange

' No longer used. No need to change g. to gb..:  7may08
Dim gRangeNormal As Boolean ' InitVoiceRange
Dim gRangeLowerSoprano As Boolean ' InitVoiceRange
Dim gRangeLowerAlto As Boolean ' InitVoiceRange
Dim gRangeLowerTenor As Boolean ' InitVoiceRange
Dim gRangeLowerBass As Boolean ' InitVoiceRange
Dim gRangeMax As Boolean ' InitVoiceRange

' Notice that the size of gChord() depends strongly
' on the ranges of the four voices, eg
'   OP: RANGE   SOPRANO G4-A5
' and the number of modulation keys in
'   gsong(gsonglast).key()
'Const CHORDSMAX As Integer = 500
'Const CHORDSMAX As Integer = 2000 ' use this value
'Const CHORDSMAX As Integer = 5000 ' use this value
'Const CHORDSMAX As Integer = 6000 ' use this value
Const CHORDSMAX As Integer = 9999 ' use this value
' if boolean gRangeMax is used

' Logically divide gChord() into a Lower Chord Table
' and an Upper Chord Table.  See sub EditChord.
Const LOWERCHORDSMIN As Integer = 1
Const LOWERCHORDSMAX As Integer = CHORDSMAX - SONGNOTEMAX
Const UPPERCHORDSMIN As Integer = CHORDSMAX - SONGNOTEMAX + 1
Const UPPERCHORDSMAX As Integer = CHORDSMAX

Dim gChord(0 To CHORDSMAX + 1) As ChordRec
' First valid gChord starts at index=1.
' gChord(0) is used for scanning gChord(1-n).
' gChord(CHORDSMAX + 1) is used for re-initializing

Dim gChordLast As Integer ' index of last/highest
'   chord used for this Kn: key.
Dim gChordLastSave As Integer ' used by WriteAllChords
Dim gChordLastUpper As Integer ' index of last/highest
'   chord used in the Upper Chord Table.
Dim gChordLastUpperSave As Integer ' used by WriteAllChords?

' gChord(CHORDSMAX + 1) is only used for a dummy chord
' for fcnbCandChord to pass ch2 to fcnbParallel
' in case the note after gNoteX does not have 4 tones.

' Constants for gChord(x).DblType values,
' and indexes for gDblType
Const DBLROOT As Integer = 1 ' dbl root in bass
Const DBLTHIRDPRIMRP As Integer = 2 ' dbl THIRD
    ' AND primary tone, minor triad, root pos
Const DBLTHIRDRP As Integer = 3 ' dbl third, minor triad, root pos
Const FINALTRIPLE As Integer = 4 ' final triple root
Const DBLPRIMSOP As Integer = 5 ' dbl soprano
    ' AND primary tone, 1st inv
Const DBLSOPRANO As Integer = 6 ' dbl soprano, 1st inv
Const DBLPRIMARY As Integer = 7 ' dbl primary tone
    ' 1^ 4^ 5^, 1st inv
Const DBLPRIMTHIRDMINOR As Integer = 8 ' dbl THIRD
    ' AND primary tone, minor triad, 1st inv
Const DBLROOTMINOR As Integer = 9 ' dbl third, minor triad, 1st inv
Const DBLTHIRDMINOR As Integer = 10 ' dbl third, minor triad, 1st inv
Const DBLFIFTHMINOR As Integer = 11 ' dbl third, minor triad, 1st inv
Const DBLROOTMAJOR As Integer = 12 ' dbl third, major triad, 1st inv
Const DBLTHIRDMAJOR As Integer = 13 ' dbl third, major triad, 1st inv
Const DBLFIFTHMAJOR As Integer = 14 ' dbl third, major triad, 1st inv
Const DBLFIFTH As Integer = 15 ' dbl fifth, 2nd inv
Const DBLDIMINISH As Integer = 16 ' diminished triad:
    ' soprano 5th, else bass 3rd
'Const SUPERTONIC7TH As Integer = 17 ' no dbl, ii7 7th chord
Const Major7th As Integer = 17 ' no dbl, ii7 7th chord
Const DOMINANT7TH As Integer = 18 ' no dbl, V7 7th chord
Const HALFDIM7TH As Integer = 19 ' no dbl, V7 7th chord
Const DIMDIM7TH As Integer = 20 ' no dbl, V7 7th chord
Const TRIPLEROOT As Integer = 21 '

Const DBLTYPEMAX As Integer = 21
Dim gDblNames(DBLTYPEMAX) As String
Dim gDblDescs(DBLTYPEMAX) As String

' This array holds the links to the different
' types of ChordRec recs in the array gChord,
' corresponding to gChord(x).DblType.
Private Type DblTypeRec
    Top As Integer ' index of first rec in chain
    Bot As Integer ' index of last rec in chain
    Num As Integer ' number of links in chain
    TotVal As Integer ' number of valid chords found
    ' for all Scale degrees.
End Type
Dim gDblType(DBLTYPEMAX) As DblTypeRec

Dim gTotChordsChecked As Integer
Dim gTotChordsValid As Integer
Dim gGrandTotChordsValid As Long
Dim gTotValidType As Integer
Dim gbShowHarmony As Boolean ' False=show input
Dim gbShowValidCands As Boolean ' False=show all
Dim gbShowKeyAccidentals As Boolean ' False=show all
Dim gbWriteAll As Boolean ' False=write one file only
Dim gbWriteRow4 As Boolean ' false=write BASSROW4

Const GOBACKSMAX As Integer = 1000
Const GOBACKSMOD As Integer = 100
Dim gNumGoBacksMax As Long ' in sub Harmonize
Dim gNumGoBacksMod As Long ' in sub Harmonize
Dim gNumGoBacks As Long ' in sub Harmonize
Dim gbCalledbyPrevSol As Boolean
Dim gDNoteVoice$ ' display in this voice
Dim gDNoteRequest$ ' display chords with this note eg #B3
Dim gbShowInputNotesOnly As Boolean ' 0=false is
' default is display harmony, else input notes only

Const NOTETABLEMAX As Integer = 100
Private Type NoteTableRec
    MeasureNo As Integer ' # of measure bars + 1
    Note As Integer ' note within this measure
End Type
Dim gNoteTable(1 To NOTETABLEMAX) As NoteTableRec

' Used by InputFileName and cmdNextFile 16aug08
Dim gbFileNameNext As Boolean
Const FILENAMESMAX As Integer = 50
'  Types: eg gDirSongs or gFile or ...
Dim gFileTypes(FILENAMESMAX) As String
' Names: eg "Asn511a"
Dim gFileNames(FILENAMESMAX) As String
Dim gFileNamesDir As String
Dim gFileNamesUsed As Integer
Dim gFileNamesCurr As Integer

'***********************************************

' BEGIN ABCPLUS DEFINITIONS

' From the line "%ABC BEGIN" to the line"%ABC END",
' read in all the ABC music language text lines
' and store here.
' The following constants are used in gxAbcTable
' and are set in Sub InitAbcTable
' Pre-Note Opens:
Const OPAREN As Integer = 1 ' "(" beg phrase mark
Const OSLUR As Integer = 1 ' "(" beg phrase mark
Const OBRACE As Integer = 2 ' "{" beg grace notes
Const OBRACKET As Integer = 3 ' "[" beg chord notes
' Pre-Note Closes:
Const CBRACE As Integer = 4 ' "{" end grace notes
Const CBRACKET As Integer = 5 ' "]" end chord notes
' Pre-Note:
Const ADQUOTE As Integer = 7 ' '"' enclose text
Const ASTACCATO As Integer = 8 ' .A
Const ALETTER As Integer = 9 ' H-Y or h-w
' In-Note:
Const ACCIDEN As Integer = 11 ' _A =A ^A
Const ANOTE As Integer = 12 ' A-G, a-g, rest=z,
' or an invisible rest=x (?)
Const ACOMMA As Integer = 13 ' ","  octave
Const AQUOTE As Integer = 13 ' "'"  octave
' Time:
Const ADIGIT As Integer = 21 ' 0-9
Const ASLASH As Integer = 22 ' "/"
Const ABROKEN As Integer = 23 ' "<..." or ">>..."
Const ALT As Integer = 23 ' "<..."
Const AGT As Integer = 23 ' ">>..."
' Post-Note:
Const ATIE As Integer = 31 ' "-" tie to same pitch
Const ASPACE As Integer = 32 ' "y" extra space in
' the score, but is not followed by a length
Const CPAREN As Integer = 33 ' ")" end phrase mark
Const CSLUR As Integer = 33 ' ")" end phrase mark
Const ABLANK As Integer = 34 ' " " beam if no blank
' Bar-Chars:
Const ABAR As Integer = 41 ' "|"
Const ACOLON As Integer = 42 ' ":"
'Const OBRACKET As Integer = 3 ' "[" beg chord notes
'Const CBRACKET As Integer = 5 ' "]" end chord notes
'Const ADIGIT As Integer = 21 ' 0-9
'Const ASLASH As Integer = 22 ' "/"
' Modifier Char:
Const ASTERISK As Integer = 43 ' "*" (* -* OR z*
' extends the ( - or z to all voices
' END Flag Chars:
Const AEOS As Integer = 51 ' &HFE
Const AEOF As Integer = 52 ' &HFF
Dim gxAbcTable(0 To 255) As Byte


' gAbcState:
Const NOT_NOTE As Integer = 0
Const PRE_NOTE As Integer = 1
'Const NOTE_NAME As Integer = 2
'Const NOTE_TIME As Integer = 3
'Const POST_NOTE As Integer = 4
'Const END_NOTE As Integer = 5
'Const BAR_NOTE As Integer = 6
'Const END_SONG As Integer = 7

Const NOTE_ACCID As Integer = 2
Const NOTE_NAME As Integer = 3
Const NOTE_TIME As Integer = 4
Const POST_NOTE As Integer = 5
Const END_NOTE As Integer = 6
Const BAR_NOTE As Integer = 7
Const END_SONG As Integer = 8
' Flag passed to sub GetAbcString:
Const SAMECHAR As Integer = 1
Const SAMETYPE As Integer = 2
Const TILLCHAR As Integer = 3
Const TILLTYPE As Integer = 4
Dim gAbcState As Integer
Dim gAbcPrevState As Integer

Const TAB1 As String = &H9 ' Hex 09 is chr$(9)=TAB
Const ABCHDRMAX As Integer = 50
Dim gAbcHdr(ABCHDRMAX)
Dim gAbcHdrLast As Integer ' index of last/highest used

Dim gAbcNotes(SOPRANO To BASS) As String
Dim gAbcTimes(SOPRANO To BASS) As String
' needed?
Dim gLastBarPos(SOPRANO To BASS) As Integer
Dim gBarsFound(SOPRANO To BASS) As Integer

Const MAXINTEGER As Integer = 32767
Const EOS As String = &HFE ' end of string
Const EOFILE As String = &HFE ' end of file

Dim gbAbcInputOption As Boolean
Dim gbNHTInputOption As Boolean
Dim gbAbcInputVoice As Boolean
Dim gbAbcInputLyric As Boolean
Dim gbOneLyricLinePerVoice As Boolean
Dim gAbcVoice As Integer 'current voice being input

Private Type AbcInputRec
    String As String
    Pos As Integer
    bUsed As Boolean ' false if no notes for this voice
End Type

Const BARSPERLINE As Integer = 2
Dim gBarsPerLine As Integer
' Concatenate ABCPlus V:n lines for conversion
' to 4Part format:
Dim gAbcInput(SOPRANO To BASS) As AbcInputRec
' ABCPlus V:n non-TAB lines are
' converted to 4Part format with TAB's:
' Vn:[TAB]note1[TAB]note2...
' TM:[TAB]time1[TAB]time2... (consolidated times)
' Tn:[TAB]time1[TAB]time2... (times for Vn: notes)
' Concatenate ABCPlus i: lines for conversion
' to 4Part format:
Dim gAbcInputLyric(SOPRANO To BASS) As AbcInputRec

Dim gAbcV(SOPRANO To BASS) As AbcInputRec ' V1:-V4:
Dim gAbcT(SOPRANO To BASS) As AbcInputRec ' T1:-T4:
Dim gAbcTM As AbcInputRec ' TM: consolidated times
' from all the T1: to T4: lines.
Dim gAbcI(SOPRANO To BASS) As AbcInputRec ' Lyrics
Dim gAbcNoteNo As AbcInputRec ' % note nos comment
Dim gAbcMeasNo As AbcInputRec ' % meas nos comment

Const MAXABCBUF As Integer = 80
Private Type AbcBufRec
    xByte1(1 To MAXABCBUF) As Byte
    Num As Integer
End Type
' Temporary buffer for each voice for
' Sub PushAbcCh to push a char back on.
' Probably rarely use more than one push back.
Dim gAbcBuf(SOPRANO To BASS) As AbcBufRec

' All the pos(ition) variables in a measure
' are given in units of 1/32 notes.  That is,
' there are 32 1/32 notes in every whole note.
' A measure may be longer than a whole note.
Dim gLowPos As Integer
Dim gLowVoice As Integer
Private Type AbcFieldRec
    Abc As String
    Accid As String ' Name field only
    FourPart As String
    Pos As Integer
    Len As Integer
End Type

Private Type AbcNoteRec
    Pre As AbcFieldRec
    Name As AbcFieldRec
    Time As AbcFieldRec
    Post As AbcFieldRec
    bBarNote As Boolean
    MeasNo As Integer
    'MeasPos As Integer
    'NoteNo As Integer
    'NoteLen As Integer
    bBeamToNext As Boolean
    Lyric(1 To 4) As String
End Type
Dim gAbc(SOPRANO To BASS) As AbcNoteRec

' END ABCPLUS DEFINITIONS

'***********************************************

Dim dummy0 As Integer

Private Sub AProcedureMap()
' Keep a working copy of this non-executing proc in
'   C:\VB4PRG\4PART\4PMap.TXT
' for convenience in updating when both code frames
' of VB are in use.

' This is not an executing procedure.
' It contains a map
' of the procedure and function calls.

' Sub Form_Load()
'    call Init4Part
    '    call SetGrid4PartLabels
    '    call SetGridExtraLabels
    '    calls sub InitScales ' only once
    '    calls InitDoubleNames ' call only once
    '    calls sub InitAbcTable ' only once

'    call Main4Part
    '   call InputLines
    '       call StoreSong(gTextLine)
    '           call FillWithKeys
    '       call PropagateOptions ' after gSongLast is set
    '       call ParseTabs(gTextLine As String)
    '       call StoreNotes(gTextLine As String)
    '           [gKeyName =, gKeyMode =]
    '           fcnbKeyGood
    '           TODO move the calls that are called by both
    '           sub InitChordsForKey (in cmdChords_Click) and
    '           sub StoreNotes to a new Init sub:
    '           [ TODO check if gKeyName or
    '              gKeyMode has changed:]
    '           call InitHalfStepNames ' [gKeyName]
    '           was set in InitHalfStepNames.]
    '           call InitVoiceRange ' [gHalfStepName()
    '               was set in InitHalfStepNames.]
    '               fcnToIx
    '           (If Not gbScalesSetup then
    '               call InitScales ' only once
    '           End If - not needed)
    '           call StoreOption(gTextLine)
    '              call StoreOptionInclude(gTextLine)
    '              call StoreOptionRange(gTextLine)
    '           call StoreVoiceVn(SOPRANO, .LastV1, _
    '                        gTextLine)
    '               fcnPack(.Name)
    '               fcnToIx(fcnPack(.Name))
    '           call StoreTM(.LastTM, gTextLine)
    '           call StoreRN(.LastRN, gTextLine)
    '               [gKeyMode]
    '           call StoreFB(.LastFB, gTextLine)
    '   call DisplaySong(SongNo As Integer)
    '   call DisplayNotes(SongNo As Integer)
    '       call cmdClear_Click
    '            call ClearTable4P
    '            'call ClearExtra
    '       call SetGrid4PartLabels
    '   call cmdHarmony_Click

' The program now waits for command button actions.
' Form command buttons:

' Sub cmdAlign_Click - Center the song values

' Sub cmdChords_Click ' create a table of chords
'    for the current song key.
'    [gKeyName=, gKeyMode=]
'    call WriteAllChords (if Title="Write all chords")
    '   call InitChordsForKey - see below
    '   call MainChordsForKey - see below
    '   call WriteDblTypes
        '   fcnbPrtBool
'    call InitChordsForKey ' [If gKeyName or
'       gKeyMode has changed:]
    '   [gKeyNote =, gKeyIndex =]
'       TODO move the calls that are called by both
'       sub InitChordsForKey and sub StoreNotes
'       to a new Init sub.
    '   call InitHalfStepNames ' [gKeyName]
    '   fcnToIx(gKeyNote)
    '   (call InitScales - not needed)
    '   call InitDoubleNames
    '   call InitNoteNames ' [gHalfStepName()
    '       was set in InitHalfStepNames.]
    '   (call InitVoiceEffort - not needed or used)
    '   call InitPrimaryTones ' [gKeyIndex]
    '   call InitNeverDouble ' [gKeyIndex, gKeyMode]
'   [If gKeyName or gKeyMode changed:]
'    call MainChordsForKey [If gKeyName changed]
    '   [gKeyMode, gScaleDeg]
    '   call InitTriads
    '       [gKeyIndex, gKeyMode, gScaleDeg]
    '       [gRomNumName =, gQuality =]
    '   call ScanTriads
    '      fcnbBump
    '      fcnGreater
    '      fcnbValidChord
    '      call EditChord
    '           (fcnVoiceEffort("B",B) - not used)
    '           fcnGetFBName(gInversion,gbSeventh)
    '      call Drop (gTriads(S))
    '   call PrintDblTypes ' [gScaleDeg]
    '      fcnbPrtBool(.bBassClose, "BC", "BO")


' Sub cmdClear_Click - Clear the song form
'       call ClearTable4P
'       'call ClearExtra - not needed here

' Sub cmdExit_Click - Exit the program

' Sub cmdHarmony_Click ' fill the song table with
'   four part harmony chords for the current
'   song key.
    '   call InitProgressions ' major or minor
    '   call InitErrorAllow ' allowed for this song
    '   (call cmdChords_Click - see Harmonize)
    '   call SetNoteFlags(FirstNote, LastNote)
    '   call Harmonize(FirstNote, LastNote,
    '       gAfterX, Err)
    '   call DisplayHarm(gSongCurr)
    '       fcnNote2Col [can be used here]

' Sub Harmonize(pFirstNote As Integer, _
    '           pLastNote As Integer, _
    '           pAfterX As Integer,pErr As String)
    '   call cmdChords_Click ' see if chords change
    '   call FinalTypeChord ' [gKeyMode gRomNum]
    '       fcnbCandChord(I, J)
    '           fcnbParallel(I, I2, Errs)
    '           fcnbHarmProgOk(xChordFlag, CandFlag)
    '               fcnGetNoteBefore
    '           call Printtest
    '           call Printtest2
    '           fcnbFindChord  (only use, commented out)
    '       call ChooseLastChord
    '   call FirstTypeChord
    '   call PrevTypeChord
    '       call ChordProgressions(PrevRomNum())
    '       fcnbCandChord(I, J)
    '           fcnbParallel(I, I2, Err)
    '           fcnbHarmProgOk(xChordFlag, CandFlag)
    '               fcnGetNoteBefore
    '           call Printtest
    '           call Printtest2
    '           fcnbFindChord  (only use, commented out)
    '       call CalcVariance ' [gNoteX; gAfterX]
    '       call ChoosePrevChord
    '           fcnCol2Note(15)
    '   call Check_6_3_4
    '   fcnbNextBest(LastNote, Note)
    '   call DisplayCandChords ' for .LastCand
    '       call ClearExtra ' clear grid to blanks
    '       call SetGridExtraLabels


' Sub cmdNextSong_Click - Display the next song
    '   call DisplaySong(SongNo) ' same as above
    '   call DisplayNotes(SongNo) ' same as above
    '       call cmdClear_Click
    '       call SetGridExtraLabels
    '           ' reset col num's
    '   call cmdHarmony_Click

' Sub cmdPrevSong_Click - Display the previous song
    '   call DisplaySong(SongNo) ' same as above
    '   call DisplayNotes(SongNo) ' same as above
    '       call cmdClear_Click
    '            call ClearTable4P
    '       call SetGrid4PartLabels
    '   call cmdHarmony_Click

' Sub cmdNextNote_Click - Display the next note
    '   call DisplayCandChords ' same as above
    '       call ClearExtra ' clear grid
    '       call SetGridExtraLabels


' Sub cmdPrevNote_Click - Display the previous note
    '   call DisplayCandChords ' same as above
    '       call ClearExtra ' clear grid
    '       call SetGridExtraLabels


' Sub cmdNextSolution_Click - display next solution
    '   call ChooseLastChord(gNoteX)
    '   call ChoosePrevChord(gNoteX)
    '   call DisplayCandChords ' same as above
    '       call ClearExtra ' clear grid
    '       call SetGridExtraLabels

    '   call Harmonize(FirstNote, LastNote,
    '       gAfterX, Err)
    '   call DisplayHarm(gSongCurr)

' Sub cmdPrevSolution_Click - display prev solution
    '   call ChooseLastChord(gNoteX)
    '   call ChoosePrevChord(gNoteX)
    '   call DisplayCandChords ' same as above
    '       call ClearExtra ' clear grid
    '       call SetGridExtraLabels
    '   call Harmonize(FirstNote, LastNote,
    '       gAfterX, Err)
    '   call DisplayHarm(gSongCurr)

' Sub cmdShow_Click
    '   call cmdHarmony_Click
    ' OR
    '   call DisplayNotes(gSongCurr) ' same as above
    '       call cmdClear_Click
    '            call ClearTable4P
    '            'call ClearExtra
    '       call SetGrid4PartLabels

' Sub cmdValidCand_Click - toggle between
'       displaying only the error-free candidates
'       and every one.

' Sub cmdWriteChord_Click - After "cmdChords", write out
'    the table of chords for the current song key.
    '   call WriteDblTypes
    '       fcnbPrtBool(.bBassClose, "BC", "BO")

' Sub cmdWriteAbc_Click
'       call WriteAbc(gSongCurr)
'          call DisplayText(E$)
'          Note2Abc(A$, Row, B$, Err)
'          fcnRepChar(A$, " ", "~")

' Sub cmdWriteCsv_Click
'       call WriteCsv(gSongCurr)
'          call DisplayText(E$)

' Sub DisplayCandChords()
    '   call sub ClearExtra
    '   call sub SetGridExtraLabels
    '   call sub cmdChords_Click


' Sub txtMyCand_KeyPress(KeyAscii As Integer)
'       call cmdNextSolution_Click ....

' Sub txtNoteNo_KeyPress(KeyAscii As Integer)
'       call cmdPrevNote_Click ....


' Sub txtSongNum_KeyPress(KeyAscii As Integer)
'       call cmdNextSong_Click ....


' ******************************

' Following samples are NOT used:
'   call SetColWidth
'   call SetRowHeight
'   temp

End Sub

Private Sub AConventions()
' Keep a working copy of this non-executing proc in
'   C:\VB4PRG\4PART\4PLog.TXT
' for convenience in updating when both code frames
' of VB are in use.

' From C:\VB4PRG\4PART\4PartD.VBS,
' as of 9/3/3 and updated,
' because I deleted proc AConventions by mistake
' from C:\VB4PRG\4PART\4PartD2.VBS on 9/12/3.

' This is not an executing procedure.
' It contains comments, logs and
' programming habits.

' This Visual Basic 4 program, PrintNote1.vbp,
' creates all possible four-part chords in
' a given key, e.g. G, for the normal range of
' chorale voices: Soprano, Alto, Tenor and Bass.
' At first, only Roman numeral I of the given key
' is done.  A number of useful traits of
' each chord is given in the gChords array.

' From HELP on "variable':
' When variables are initialized, a numeric variable
' is initialized to 0, a variable-length string is
' initialized to a zero-length string, and a
' fixed-length string is filled with zeros.
' Variant variables are initialized to Empty.
' Each element of a user-defined type variable is
' initialized as if it were a separate variable.

' Hint.  When using Option Explicit,
' type each variable in lower case, and if it
' doesn't convert to the proper initial capitals,
' then you must have misspelled it!

' Hint.  Hold down CNTL+down arrow to take you to
' the last procedure that you added.  Hold down
' CNTL+up arrow to take you to the first one made.

' Hint - It is good to se Type record definitions
'       because if you mistakenly use the wrong
'   .name in a Type record, VB will point this
'   out: Member or data member not found
'   As opposed to testing or setting the
'   wrong variable without a clue.

' The program begins with: Sub Form_Load().

' My conventions:
' Prefix global variables with "g", except CONST's
' and Type statement variables.
' Prefix boolean variables with b, and
' global booleans with gb, except functions.
' Rename byte variables that boolean arithmetic is
' done on, e.g. bA AND NOT bB, from bZZZ to xZZZ, where
' the leading lower case x means hexadecimal.

' Short local integers like I, J, K should be
' limited to a short routine or gosub, fitting easily
' on one screen.  Do not pass I to a GOSUB routine;
' use a more descriptive name since you are going to a
' different screen.

' Define mostly integers and strings.
' Explicitly define string instead of defaulting
' to type Variant.
' Use the $ form of string functions for clarity
' and maybe efficiency.
' Uppercase CONST's.
' call each routine with an explicit "CALL",
' so that is is easy to FIND all of them.
' Prefix each function with "Fcn" for FIND's.
' Add a comment " ' call" after each written
' function call to easily FIND them.

' At the beginning of each code procedure, put
    ' called by sub X
    ' Globals used: gX
    ' Globals set: gX =
' Make a module map of what calls what.
' Keep each routine short.
' Organize longer procedures into a main section
' with mostly GOSUB's and GOSUB routines separated
' by a line of asterisks.
    '****************************************
' (Indent 2nd and 3rd order GOSUB's to show
' structure - probably not.)
' Try to return from every Err gosub to the main line.
' Avoid exiting from way inside the procedure.

' Hide details that other programs do not need
' to know in the lowest level module.
' Passed arguments should not be modified by
' the called modules, except as designed.
' So, pass by value, "ByVal", except for long
' strings and variants.  Default:pass by reference.
'
' In a minor triad of a major or minor key, with the
' root or third in the bass, the third is often doubled,
' especially if 1^, 4^ or 5^, as it sounds more "minor".
'
' In a major triad of a minor key, with the third in
' the bass, Bach sometimes doubled the third, especially
' if 1^, 4^ or 5^, as it sounds more "major". p.184

' Hint - when the program is stopped, double click on
'   a command button to enter its code.

' WARNING - Do only ONE "AND" BIT test on a BYTE
'           variable (as opposed to a BOOLEAN)
' at a time.
'   The result of a bit operation on a BOOLEAN
' variable is True (-1) or False (0).
'   The result of a bit operation on a BYTE variable
' is an integer. False if result=0, else True
' if the result is any other integer.
'   The only way to test more than two BYTE
' operations at a time is to test that each of
' them is false by testing that all together = 0.
'   If you want to test that all of A,B,C are FALSE,
'   IF A AND B AND C = 0
' will work.
' E.g. Gosub NHTSolution in Sub Harmonize.

End Sub

Private Sub InitNaturals()
    ' called by sub InitChordsForKey
    
    ' Globals used: gKeyIndex, gKeyMode
    ' Globals set: gNeverDouble()

    ' Enter with gKeyName, gKeyMode, and
    ' gKeyIndex, the half-step index
    ' to the lowest note of the Key, e.g. "G#1",
    ' in gNoteName.

    ' Add natural signs "=" to gNoteName to
    ' provide for reversing the sharps or flats
    ' in the original key signature of the song
    ' or the current modulation key signature.
    ' Each note has the current key no in
    ' gNote(i).SongKey.  It is used in InputNotes
    ' and in Harmonize.
    
    ' Notice -
    ' 1) If Bb and Cb are in the key signature, as in
    ' Gb major with 6 flats, do not change
    ' Cb to =B in gNoteName, for the
    ' composer would naturally use Cb instead of =B.
    
    ' 2) If Eb and Fb are in the key signature, as in
    ' Cb major with 7 flats, do not change
    ' Fb to =E in gNoteName, for the
    ' composer would naturally use Fb instead of =E.
    
    ' 3) If B# and C# are in the key signature, as in
    ' C# major with 7 sharps, do not change
    ' B# to =C in gNoteName, for the
    ' composer would naturally use B# instead of =C.
    
    ' 4) If E# and F# are in the key signature, as in
    ' F# major with 6 sharps, do not change
    ' E# to =F in gNoteName, for the
    ' composer would naturally use E# instead of =F.
    
    
    
    
    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim Idx As Integer
    Dim KeyName As String
    Dim KeyNote As String
    Dim KeyIndex As Integer
    Dim A$
    Dim B$
    Dim E$
    Dim Song As Integer
    Dim EnHarmonic1 As String
    Dim EnHarmonic2 As String
    Dim KeyNameEnHarm As String
    Dim KeyNoteEnHarm As String
    Dim KeyNoteName
    Dim KeyNoteRev As String
    Dim Rev$
    
    Dim Prev$
    Dim Letter$
    Dim Digit$
    Dim ThisSong As Integer
    
'*********************************************
'*********************************************
Main:
    GoSub Init
    GoSub MainLine
    'GoSub Debug2
    'GoSub Debug1
    Exit Sub

'*********************************************
'********************************************

Init:
    EnHarmonic1 = "DbEbFbGbAbBbCb"
    EnHarmonic2 = "C#D#E#F#G#A#B#"
    If gSongCurr > 0 Then
        ThisSong = gSongCurr
        I = I
    Else
        ThisSong = gSongLast
        I = I
    End If
    GoSub Test1
    Return
    
'********************************************
Test1:
        If ThisSong = 4 Then
            I = I
        End If
        Return
'********************************************

MainLine:
    With gSong(ThisSong)
        For Idx = 1 To .KeySigUsedIst
            KeyName = .KeySigIst(Idx)
            GoSub GetKeyIndex
            GoSub AddNaturalSigns
        Next Idx
        For Idx = 1 To .KeySigUsed
            KeyName = .KeySig(Idx)
            GoSub GetKeyIndex
        Next Idx
    End With
    'GoSub Debug3
    Return
    
'*********************************************
' Notice that in a minor key the raised 6th and 7th
' may have a leading # or b.  So reverse the two
' chars in A$ and save in B$.
AddNaturalSigns:
    For I = KeyIndex To NOTESMAX Step 12
        K = Len(gNoteName(I)) - 1
        A$ = Left$(gNoteName(I), K)
        B$ = Right$(A$, 1) & Left$(A$, 1) ' reverse
        If A$ = KeyName Then
            KeyNoteName = A$
        ElseIf A$ = KeyNameEnHarm Then
            KeyNoteName = A$
        ElseIf B$ = KeyName Then
            KeyNoteName = B$
        Else
            GoSub Err2
            GoTo Exit1
        End If
        If Right$(KeyName, 1) = "b" Then
            B$ = gNoteName(I + 1)
            ' keep the spellings Bb, Cb, C and Eb, Fb, F:
            If Len(B$) = 2 Then '4aug08
                gNoteName(I + 1) = "=" & B$
            End If
        ElseIf Right$(KeyName, 1) = "#" Then
            If I > 0 Then ' check for "C0"
                B$ = gNoteName(I - 1)
                ' keep the spellings =B, B#, C# and =E, E#, F#:
                If Len(B$) = 2 Then '4aug08
                    gNoteName(I - 1) = "=" & B$
                End If
            End If
        Else
            GoSub Err2
            GoTo Exit1
        End If
    Next I
Exit1:
    Return
    
'*********************************************

GetKeyIndex:
    KeyNote = KeyName & "0"
    If KeyNote = "Cb0" Then
        I = I
    End If
    ' Use function fcnToIx to return the index of
    ' the note, e.g. G#0, in gNoteName.
    gbfcnToIxErrMsg = False
    KeyIndex = 0
    KeyIndex = fcnToIx(KeyNote) ' call
    If KeyIndex = 99 Then
        KeyNote = KeyName & "1" ' eg Cb1
        KeyIndex = fcnToIx(KeyNote) ' call
        
        If KeyIndex = 99 Then
            Rev$ = Right$(KeyName, 1) & Left$(KeyName, 1)
            KeyNoteRev = Rev$ & "0"
            KeyIndex = 0
            KeyIndex = fcnToIx(KeyNoteRev) ' call
                
            If KeyIndex = 99 Then
                KeyNoteRev = Rev$ & "1" ' eg bC1
                KeyIndex = 0
                KeyIndex = fcnToIx(KeyNoteRev) ' call
                
                If KeyIndex = 99 Then
                    GoSub GetEnharmonic
                    KeyIndex = 0
                    gbfcnToIxErrMsg = True
                    KeyIndex = fcnToIx(KeyNoteEnHarm) ' call
                    
                    If KeyIndex = 99 Then
                        GoTo Err1
                    End If
                End If
            End If
        End If
    End If
    Return

'*********************************************

' If Bb0 is not in the new key, then try A#0,
' e.g. when a flat key is switched to C major.
GetEnharmonic:
    Select Case Right$(KeyName, 1)
        Case "b"
            J = InStr(EnHarmonic1, KeyName)
            If J > 0 Then
                KeyNameEnHarm = Mid$(EnHarmonic2, J, 2)
                KeyNoteEnHarm = KeyNameEnHarm & "0"
            Else
                GoSub Err3
            End If
        Case "#"
            J = InStr(EnHarmonic2, KeyName)
            If J > 0 Then
                KeyNameEnHarm = Mid$(EnHarmonic1, J, 2)
                KeyNoteEnHarm = KeyNameEnHarm & "0"
            Else
                GoSub Err3
            End If
        Case Else
            GoSub Err3
        End Select
    Return

'*********************************************

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "InitNaturals:E1 invalid KeyNote="; _
        KeyNote; " in song no="; ThisSong
    GoSub Debug2
    GoSub Debug1
    I = I
    Stop
    Return

'*********************************************

Err2:
    gInputErrs = gInputErrs + 1
    Debug.Print "InitNaturals:E2 invalid KeyName="; _
        KeyName; " in song no="; ThisSong
    GoSub Debug2
    GoSub Debug1
    I = I
    Stop
    Return
    
'*********************************************

Err3:
    gInputErrs = gInputErrs + 1
    Debug.Print "InitNaturals:E3 invalid KeyName enharmonic="; _
        KeyName; " in song no="; ThisSong
    GoSub Debug2
    GoSub Debug1
    I = I
    Stop
    Return
    
'*********************************************

Debug1:
    Debug.Print "InitNaturals:D1 "
    K = gKeyIndex
    For J = 0 To 7
        For I = 0 To 11
            If K < 96 Then
                Debug.Print K; gNoteName(K);
                K = K + 1
            End If
        Next I
        Debug.Print
    Next J
    Debug.Print K; gNoteName(K)
    Return


'**********************************************
    
Debug2:
    E$ = "InitNaturals:D2 " _
        & "accidentals for key=" _
        & gKeyName & " for mode=" & gKeyMode _
        & " in song=" & ThisSong
    Call DisplayText(E$)
    Debug.Print E$
    Return

'**********************************************
    
Debug3:
    Debug.Print "InitNaturals:D3 "; _
        " gKeyName gKeyMode gKeyIndex="; _
        gKeyName; gKeyMode; gKeyIndex
    With gSong(ThisSong)
        Debug.Print "InitNaturals:D3 .KeySigUsedIst=";
        For I = 1 To .KeySigUsedIst
            Debug.Print I; .KeySigIst(I);
        Next I
        Debug.Print
        Debug.Print "InitNaturals:D3 .KeySigUsed=";
        For I = 1 To .KeySigUsed
            Debug.Print I; .KeySig(I);
        Next I
        Debug.Print
    End With
    Return
End Sub

Private Function fcnbEnharmonic(pInName As String, _
                                pOutName As String)
    ' called by sub InitChordsForKey
    
    ' Globals used:
    ' Globals set:

    ' Given pInName = notename Bb ending with "b",
    ' return pOutName = enharmonic notename A# ending
    ' with "#", and vice versa.
    
    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim Song As Integer
    Dim E$
    
    Dim EnHarmonic1 As String
    Dim EnHarmonic2 As String
    
'*********************************************
'*********************************************
Main:
    GoSub Init
    GoSub MainLine
    If pOutName = "" Then
        fcnbEnharmonic = False
        'GoSub Debug2
        'GoSub Debug1
    Else
        fcnbEnharmonic = True
    End If
    Exit Function

'*********************************************
'********************************************

Init:
    EnHarmonic1 = "DbEbFbGbAbBbCb"
    EnHarmonic2 = "C#D#E#F#G#A#B#"
    Return
    
'********************************************

MainLine:
    pOutName = ""
    Select Case Right$(pInName, 1)
        Case "b"
            J = InStr(EnHarmonic1, pInName)
            If J > 0 Then
                pOutName = Mid$(EnHarmonic2, J, 2)
            Else
                GoSub Err3
            End If
        Case "#"
            J = InStr(EnHarmonic2, pInName)
            If J > 0 Then
                pOutName = Mid$(EnHarmonic1, J, 2)
            Else
                GoSub Err3
            End If
        Case Else
            GoSub Err3
    End Select
    Return

'*********************************************

Err3:
    gInputErrs = gInputErrs + 1
    Debug.Print "fcnbEnharmonic: invalid pInName enharmonic="; _
        pInName; " in song no="; gSongLast
    GoSub Debug2
    GoSub Debug1
    I = I
    'Stop
    Return
    
'*********************************************

Debug1:
    Debug.Print "fcnbEnharmonic: "
    K = gKeyIndex
    For J = 0 To 7
        For I = 0 To 11
            If K < 96 Then
                Debug.Print K; gNoteName(K);
                K = K + 1
            End If
        Next I
        Debug.Print
    Next J
    Debug.Print K; gNoteName(K)
    Return

'**********************************************
    
Debug2:
    If gSongCurr > 0 Then
        Song = gSongCurr
    Else
        Song = gSongLast
    End If
    E$ = "fcnbEnharmonic: " _
        & "accidentals for key=" _
        & gKeyName & " for mode=" & gKeyMode _
        & " in song=" & Song
    Call DisplayText(E$)
    Debug.Print E$
    Return

End Function


Private Sub Note2Abc(pVoice As Integer, _
                p4Part As String, _
                pbAlto3 As Boolean, _
                pAbc As String, _
                pErr As String)
    ' called by sub WriteAbcNotes
    ' Globals used:
    ' Globals set:

    ' Does the opposite of sub AbcNoteTo4Part

    ' Convert a note name
    ' from my 4Part notation in N4Part$
    ' to ABCPlus music notation in pAbc,
    ' and return
    '   pErr=""
    ' if conversion was successful.
    ' Support all 88 piano keys from A0 to C8.

    ' Normally add 2 to the Octave number if
    ' the voice is bass or tenor.
    ' If pbAlto3 is true, then the tenor voice is
    ' going to be written in the Alto Clef for
    ' the Viola part, so do not add 2 to the Octave.

    ' In 4Part notation:
    '   C4 = Middle C.
    '   C8 = top key on piano
    '   A0 = bottom key on piano
    '   Octave C3D3...B3 is followed by
    '   octave C4D4...B4.

    ' Treble clef:
    '                     Top note on piano:
    '                                      V
    ' c'd'e'f'g'a'b' c''d''e''f''g''a''b'' c''' ABCPlus
    ' C D E F G A B  C  D  E  F  G  A  B   C    4Part
    ' 6 6 6 6 6 6 6  7  7  7  7  7  7  7   8    4Part

    '        Middle C:
    '                V
    ' C,D,E,F,G,A,B, CDEFGAB cdefgab c'   4Part
    ' C D E F G A B  CDEFGAB CDEFGAB C    4Part
    ' 3 3 3 3 3 3 3  4444444 5555555 6    4Part

    ' C,,D,,E,,F,,G,,A,,B,, C,  ABCPlus
    ' C  D  E  F  G  A  B   C   4Part
    ' 2  2  2  2  2  2  2   3   4Part

    ' C,,,D,,,E,,,F,,,G,,,A,,,B,,, C,,   ABCPlus
    ' C   D   E   F   G   A   B    C     4Part
    ' 1   1   1   1   1   1   1    2     4Part

    ' Lowest note on piano:
    ' V
    ' A,,,,B,,,, C,,,         ABCPlus
    ' A    B     C           4Part
    ' 0    0     1           4Part


    ' When using OCTAVE=-2 in ABCPlus for
    ' program abc2midi, then
    ' Bass clef:
    ' CDEFGABcdefgabc'd'e'f'g'a'b'c''  ABCPlus
    ' C      C      C             C    4Part
    ' 1      2      3             4    4Part
    ' C,D,E,F,G,A,B,C   ABCPlus
    ' C D   ...   B C   4Part
    ' 0 0   ...   0 1   4Part

    Dim I As Integer
    Dim N4Part$
    Dim Leng As Integer
    Dim A$
    Dim B$
    Dim Letter1$
    Dim Acciden1$
    Dim Digit1$
    Dim Prefix$
    Dim Body$
    Dim Suffix$
    Dim Octave As Integer
    Dim Acc4Part As Variant
    Dim AccAbc As Variant
    Dim CaseAbc As Variant
    Dim OctAbc As Variant
    Dim gbShowKeyAccidentalsSave As Boolean
    
    'Dim bNoteToAbc As Boolean

    ' Translate 4part octave to ABCPlus:
    ' Treble clef:
    'A0   C1   C2   C3   C4   C5   C6   C7   C8
    'CaseAbc = Array("",
    CaseAbc = Array( _
    "U", "U", "U", "U", "U", "L", "L", "L", "L")

    'A0      C1     C2    C3  C4  C5   C6   C7    C8
    'OctAbc = Array("",
    OctAbc = Array( _
    ",,,,", ",,,", ",,", ",", "", "", "'", "''", "'''")

    ' Translate 4part accidentals to ABCPlus:
    Acc4Part = Array("bb", "b", "=", "#", "x")
    AccAbc = Array("__", "_", "=", "^", "^^")

    ' E.g. to translate (4Part) C#3 to (ABC) ^C,
    ' the input note, N4Part$, to this sub Note2Abc
    ' should have the format:
    
    ' (1) accidental1  letter  digit
    ' where optional accidental1 is: bb b = # x
    ' which can be used in a minor key to raise
    ' the ^#3, ^#6, ^#7 (iv to IV, v to V, or Picardy)
    ' or
    
    ' (2) letter  accidental2  digit
    ' where optional accidental2 is: b #
    ' which can be used for altered notes in a
    ' key modulation, e.g. K2: G Major.
    ' In this case, use
    '   fcnHideKeyAccidentals()
    ' to hide the accidentals for all notes in the
    ' original key signature e.g. KY: C
    ' but not for any notes in the modulated key K2:.
    
    ' When the 4Part format notes are translated
    ' to the ABCPlus format, all accidentals will
    ' be shown on the ABC score except notes in the
    ' original key signature e.g. KY: C.  30apr08
    
    ' e.g. translate 4Part's C#3 to ABC's ^C, where
    ' (1) letter = A to G
    ' (2) optional-accidental(s) = bb b n # x
    ' (3) digit = 0 to 8


'*****************************************
'*****************************************

Main:
    N4Part$ = Trim$(p4Part)
    GoSub MainLine
    Exit Sub

'*****************************************
'*****************************************

MainLine:
    GoSub HideKeyAcc
    GoSub ConvPrefix
    GoSub DigitAndOctave
    GoSub ConvBody
    If pErr = "" Then
        pAbc = Prefix$ & Body$ & Suffix$
        pAbc = Trim$(pAbc)
    End If
    Return

'*****************************************

    ' If there is an # or b just before the digit
    ' at the end, then
    ' (1) remove it if the accidental is in the
    ' original key signature KY:
    ' (2) else, reverse the letter and the accidental
    ' e.g. reverse C#3 to #C3 for processing at
    ' ConvPrefix:
HideKeyAcc:
    gbShowKeyAccidentalsSave = gbShowKeyAccidentals
    gbShowKeyAccidentals = False
    A$ = N4Part$
    If A$ = "G#3" Then
        I = I
    End If
    N4Part$ = fcnHideKeyAccidentals(A$)
    Leng = Len(N4Part$)
    B$ = Mid$(N4Part$, Leng - 1, 1)
    If B$ = "#" Or B$ = "b" Then
        If Leng = 3 Then ' e.g. reverse C#3 to #C3
            A$ = B$ & Left$(N4Part$, 1) & Right$(N4Part$, 1)
            N4Part$ = A$
        ElseIf Leng > 3 Then ' unlikely
            A$ = Left$(N4Part$, Leng - 3) & B$ _
                & Left$(N4Part$, 1) & Right$(N4Part$, 1)
            N4Part$ = A$
        End If
    End If
    gbShowKeyAccidentals = gbShowKeyAccidentalsSave
    Return

'*****************************************
    
ConvPrefix:
    Prefix$ = ""
    If InStr(N4Part$, "bb") Then
        Prefix$ = AccAbc(0) ' "__"
    ElseIf Leng > 2 Then
        Acciden1$ = Mid$(N4Part$, Leng - 2, 1)
        ' Get the ABCPlus accidental corresponding
        ' to the 4Part one.
        For I = 1 To 4
            If Acciden1$ = Acc4Part(I) Then
                Prefix$ = AccAbc(I)
                Exit For
            End If
        Next I
    End If
    Return

'*****************************************

    ' Replace the 4Part accidental with the
    ' ABCPlus one.
ABCPrefix:
    For I = 0 To 4
        If Acciden1$ = Acc4Part(I) Then
            Prefix$ = AccAbc(I)
            Exit For
        End If
    Next I
    Return

'*****************************************
    ' 12. OP: MISC  TENOR   OCTAVES +2
    '     OP: MISC  BASS    OCTAVES +2
    ' E.g. add 2 octaves to the pitch of
    ' each Tenor (or Bass) note in
    ' sub Note2Abc.  ' 22sep08
Dim gTenorOctaves As Integer
Dim gBassOctaves As Integer
    ' ABCPlus notes are often entered by
    ' hand as A-G and a-g.
    

DigitAndOctave:
    Digit1$ = Right$(N4Part$, 1) ' rightmost char=digit
    If Digit1$ < "0" Or Digit1$ > "8" Then
        pErr = "Note2Abc: Out of range digit=" & Digit1$
        Exit Sub
    End If
    Octave = Val(Digit1$)
    If pVoice = 4 Then ' Bass
        'Octave = Octave + gTenorOctaves
    ElseIf pVoice = 3 Then ' Tenor or Alto
        If Not pbAlto3 Then
            'Octave = Octave + gBassOctaves
        End If
    End If
    Return
    
    
'*****************************************

ConvBody:
    ' The note letter is just before the digit:
    Letter1$ = Mid$(N4Part$, Leng - 1, 1)
    If Letter1$ < "A" _
    Or Letter1$ > "G" Then
        pErr = "Note2Abc: letter=" & Letter1
        Return
    End If
    
    If CaseAbc(Octave) = "U" Then
        Body$ = UCase$(Letter1$)
    Else
        Body$ = LCase(Letter1$)
    End If
    Suffix$ = OctAbc(Octave)
    Return
    
End Sub


Private Sub AbcNoteTo4Part(pVoice As Integer, _
                pAbc As String, _
                pAcciden As String, _
                pbAlto3 As Boolean, _
                p4Part As String, _
                pErr As String)
    ' called by sub ParseAbcNoteName
    ' Globals used:
    ' Globals set:

    ' Does the opposite of sub Note2Abc

    ' In process of converting to the opposite of
    ' sub Note2Abc 11/23/04

    ' Convert a note name from ABCPlus music notation,
    '   pAbc
    ' to my 4Part notation
    '   p4Part
    ' and return
    '   pErr=""
    ' if conversion was successful.

    ' The input ABC note, pAbc, to this sub Note2Abc
    ' should have the format: e.g. C,#
    '   Letter1 octave-comma-quote optional-accidental
    ' (1) Letter1 = A to G or a to g
    ' (2) optional-accidental(s) = __ _ = ^ ^^
    ' (3) octave-commas = 0 to 5 e.g. _C,,,
    ' (4) octave-quotes = 0 to 4 e.g. ^c''

    ' The output 4Part note, p4Part, from this
    ' sub Note2Abc should have the format: C#3
    '   Letter1  optional-accidental  octave-Digit1
    ' (1) Letter1 = A to G
    ' (2) optional-accidental(s) = bb b n # x
    ' (3) octave-Digit1 = 0 to 8

    ' Normally add 2 to the Octave number if
    ' the voice is bass or tenor.  This allows
    ' program abcm2ps to print a nice grand staff
    ' of treble and bass clefs for four voices.
    ' However,
    ' if pbAlto3 is true, then the tenor voice is
    ' going to be written in the Alto Clef for
    ' the Viola part, so do not add 2 to the Octave.

    ' Support all 88 piano keys from A0 to C8.
    ' In 4Part notation:
    '   C4 = Middle C.
    '   C8 = top key on piano
    '   A0 = bottom key on piano
    '   Octave C3D3...B3 is followed by
    '   octave C4D4...B4.

    ' Treble clef:
    '                     Top note on piano:
    '                                      V
    ' c'd'e'f'g'a'b' c''d''e''f''g''a''b'' c''' ABCPlus
    ' C D E F G A B  C  D  E  F  G  A  B   C    4Part
    ' 6 6 6 6 6 6 6  7  7  7  7  7  7  7   8    4Part

    '        Middle C:
    '                V
    ' C,D,E,F,G,A,B, CDEFGAB cdefgab c'   4Part
    ' C D E F G A B  CDEFGAB CDEFGAB C    4Part
    ' 3 3 3 3 3 3 3  4444444 5555555 6    4Part

    ' C,,D,,E,,F,,G,,A,,B,, C,  ABCPlus
    ' C  D  E  F  G  A  B   C   4Part
    ' 2  2  2  2  2  2  2   3   4Part

    ' C,,,D,,,E,,,F,,,G,,,A,,,B,,, C,,   ABCPlus
    ' C   D   E   F   G   A   B    C     4Part
    ' 1   1   1   1   1   1   1    2     4Part

    ' Lowest note on piano:
    ' V
    ' A,,,,B,,,, C,,,         ABCPlus
    ' A    B     C           4Part
    ' 0    0     1           4Part


    ' When using OCTAVE=-2 in ABCPlus for
    ' program abc2midi, then
    ' Bass clef:
    ' CDEFGABcdefgabc'd'e'f'g'a'b'c''  ABCPlus
    ' C      C      C             C    4Part
    ' 1      2      3             4    4Part
    ' C,D,E,F,G,A,B,C   ABCPlus
    ' C D   ...   B C   4Part
    ' 0 0   ...   0 1   4Part

    ' You can show the true double bass notes
    ' in the bass clef, or show its range as the
    ' same as the cello, and assume that it will
    ' be played an octave lower.

    ' After the Octave number is calculated,
    ' a lower case note letter in ABC format,
    ' e.g. c', must be uppercased in 4Part format.

    Dim AccAbc As Variant
    Dim Acc4Part As Variant
    Dim CaseAbc
    Dim OctaveAbc
    Dim SaveAbc$
    Dim Letter1$
    Dim bUpperCase As Boolean
    Dim Commas$ ' # of commas = octaves below Mid C
    Dim Quotes$ ' # of quotes = octaves above C above
    ' Middle C
    Dim Middle$
    Dim Suffix$
    Dim Octave As Integer
    Dim A$
    Dim I As Integer

    ' Treble clef:          C1    C2  C3    C4   C5
    'CaseAbc = Array("",   "U",  "U", "U", "L", "L")
    'OctaveAbc = Array("", ",,", ",", "",   "", "'")

    ' Translate 4part octave to ABCPlus:
    ' Treble clef:
    'A0   C1   C2   C3   C4   C5   C6   C7   C8
   'CaseAbc = Array( _
   '"U", "U", "U", "U", "U", "L", "L", "L", "L")

    'A0      C1     C2    C3  C4  C5   C6   C7    C8
   'OctaveAbc = Array( _
   '",,,,", ",,,", ",,", ",", "", "", "'", "''", "'''")

    ' Translate 4part accidentals to ABCPlus:
    'Acc4Part = Array("bb", "b", "n", "#", "x")
    Acc4Part = Array("bb", "b", "=", "#", "x")
    AccAbc = Array("__", "_", "=", "^", "^^")

'*****************************************
'*****************************************

Main:
    GoSub Init
    If pErr = "" Then
        GoSub MainLine
    ElseIf pErr = "Bar" Then
        pErr = "" ' normal exit
    End If
    Exit Sub

'*****************************************
'*****************************************

' TODO how to pass various ABC measure
' separators (colon, bracket, slash)
' 4Part besides just "|" or "||" ?
' in sub AbcNoteTo4Part
Init:
    'SaveAbc$ = pAbc
    pErr = ""
    pAbc = Trim$(pAbc)
    If pAbc = "" Then
        p4Part = pAbc
        pErr = "AbcNoteTo4Part: Blank note"
    ElseIf InStr(pAbc, "|") > 0 _
        Or InStr(pAbc, ":") > 0 Then
        p4Part = pAbc
        pErr = "Bar"
    End If
    Return

'*****************************************

MainLine:
    If Left$(pAbc, 1) = "c" Then
        I = I ' testing
    End If
    If Len(pAbc) = 1 Then
        Letter1$ = pAbc
        GoSub GetOctaveSingleLetter
    Else
        Letter1$ = Left$(pAbc, 1)
        If Letter1$ = UCase$(Letter1$) Then
            GoSub GetOctaveUpperCase
        Else
            GoSub GetOctaveLowerCase
        End If
    End If
    'GoSub AdjustOctave ' may be needed

    If Len(pAcciden) = 0 Then
        Middle$ = ""
    Else
        GoSub GetMiddle
    End If

    ' The function Format converts a number to
    ' a printable string without leading blank.
    Suffix$ = Format(Octave)

    ' 4Part note format:
    ' TODO 12/15/6
    '   (Letter1  optional-accidental  octave-Digit1)
    '   Input-accidental  Letter1  octave-Digit1
    'p4Part = UCase$(Letter1$) & Middle$ & Suffix$
    p4Part = Middle$ & UCase$(Letter1$) & Suffix$
    p4Part = Trim$(p4Part)
    Return

'*****************************************

GetOctaveSingleLetter:
    Commas$ = ""
    Quotes$ = ""
    If Letter1$ = UCase$(Letter1$) Then
        bUpperCase = True ' e.g. C,,
        Octave = 4 ' e.g. D
    Else
        bUpperCase = False ' e.g. c'
        Octave = 5 ' e.g. d
    End If
    Return

'*****************************************

' Upper case can be followed by 0-4 commas
GetOctaveUpperCase:
    bUpperCase = True ' e.g. C,,
    Commas$ = Mid$(pAbc, 2)
    'Quotes$ = ""
    If Left$(Commas$, 1) <> "," Then
        pErr = "AbcNoteTo4Part: missing " & _
            "comma after l/c note name=" & _
            Letter1$
        Octave = 4  ' try to go on
    ElseIf Len(Commas$) > 4 Then
        pErr = "AbcNoteTo4Part: more than 4 " & _
            "commas after note name=" & _
            Letter1$
        Octave = 4 - 4 ' try to go on
    Else
        Octave = 4 - Len(Commas$)
    End If
    Return

'*****************************************

' Lower case can be followed by 0-3 quotes
GetOctaveLowerCase:
    bUpperCase = False ' e.g. c'
    Quotes$ = Mid$(pAbc, 2)
    Commas$ = ""
    If Left$(Quotes$, 1) <> "'" Then
        pErr = "AbcNoteTo4Part: missing " & _
            "quote after u/c note name=" & _
            Letter1$
        Octave = 5  ' try to go on
    ElseIf Len(Quotes$) > 3 Then
        pErr = "AbcNoteTo4Part: more than 3 " & _
            "quotes after note name=" & _
            Letter1$
        Octave = 5 + 3 ' try to go on
    Else
        Octave = 5 + Len(Quotes$)
    End If
    Return

'*****************************************

GetMiddle:
    ' Suppress the accidental # or b in ABCPlus
    ' if it is in the key signature.
    Middle$ = ""
    A$ = Letter1$ & pAcciden
    If pAcciden = "#" Or pAcciden = "b" Then
        For I = 1 To gKeySigLast
            If A$ = gKeySig(I) Then
                GoTo GetMiddle2
            End If
        Next I
    End If

    ' Else, replace the ABCPlus accidental with the
    ' 4Part one.
    For I = 0 To 4
        If pAcciden = AccAbc(I) Then
            Middle$ = Acc4Part(I)
            GoTo GetMiddle2
        End If
    Next I
    ' this error should not happen due to
    ' earlier edits:
    pErr = "AbcNoteTo4Part: invalid " & _
        "accidental=" & pAcciden & _
        " after note name=" & _
        Letter1$
    Middle$ = "" ' try to go on

GetMiddle2:
    Return

'*****************************************

' This may be needed to adjust the Tenor and
' Bass voices when abcm2ps puts them in the
' bass clef.  See sub Note2Abc
AdjustOctave:
    If pVoice = 4 Then ' Bass
        Octave = Octave + 2
    ElseIf pVoice = 3 Then ' Tenor or Alto
        If Not pbAlto3 Then
            Octave = Octave + 2
        End If
    End If
    Return

End Sub

Private Sub Time2Abc(pMeterLeng As Integer, _
                pNoteLeng As String, _
                pAbcLeng As String, _
                pErr As String)
    ' called by sub WriteAbcNotes
    ' calls

    ' Globals used:
    ' Globals set:
    ' Convert a note length from my 4Part notation
    ' to ABCPlus music notation, and return
    ' an err msg if conversion was not successful.

    ' In ABCPlus, the length of a note follows the
    ' letter indicating the pitch, e.g.
    ' If the default ABC note length is L:1/4, a
    ' quarter note, then a quarter note middle C is
    '   C   quarter note
    '   C2  half note
    '   C3/2 is a dotted quarter note
    '   C/ or C/2 is an eighth note
    '   C// or C/4 is a sixteenth note
    ' An ABC note can be followed by one or more
    ' > or < signs.
    '   C>B means a dotted Quarter C followed by a
    '   eighth note B.
    '   C<<B means a sixteenth note followed by a
    ' double-dotted Quarter C.
    '   C2>B means a dotted half note C followed by a
    ' quarter note.
        
    ' Notice: in 4Part notation, the note length is
    ' given on the TM: or Tn: lines.
    ' A letter in WHQEST is followed by an optional
    ' integer or fraction, followed by an optional
    ' string of >>'s or <<'s.

    Dim I As Integer
    Dim Wa As Variant
    Dim Ha As Variant
    Dim Qa As Variant
    Dim Ea As Variant
    Dim Sa As Variant
    Dim Ta As Variant
    Dim LengNames As String
    Dim TMName As String
    Dim Numer1 As Integer
    Dim Denom1 As Integer
    Dim Numer2 As Integer
    Dim Denom2 As Integer
    Dim Numer3 As Integer
    Dim Denom3 As Integer
    Dim Tilde As String
    Dim Numeric As String
    Dim Arrows As String
    Dim Fraction As String
    Dim Gcd As Integer

'********************************************
'********************************************

Main:
    GoSub Init
    GoSub Main1
    Exit Sub

'********************************************
'********************************************

Init:
    ' Note length:  W   H     Q     E     S     T
    ' ABCPlus meter length is in arrays:
    'No = Array("","W","H","Q","E","S","T")
    ' Meter length: 1   2   4   8   16  32
    '      I = 0    1   2    3     4     5      6
    Wa = Array("", "", "/", "/4", "/8", "/16", "/32")
    Ha = Array("", "2", "", "/", "/4", "/8", "/16")
    Qa = Array("", "4", "2", "", "/", "/4", "/8")
    Ea = Array("", "8", "4", "2", "", "/", "/4")
    Sa = Array("", "16", "8", "4", "2", "", "/")
    Ta = Array("", "32", "16", "8", "4", "2", "")
    LengNames = "WHQEST"
    Return
    
'********************************************
    
Main1:
    'If pNoteLeng = "Q2>" Then
    '    I = I
    'End If
    If Right$(pNoteLeng, 1) = "~" Then
        Tilde = "~"
        pNoteLeng = Left$(pNoteLeng, Len(pNoteLeng) - 1)
    Else
        Tilde = "" ' 1sep08
    End If
    GoSub GetLengNameFactor
    GoSub GetArrayFractions
    GoSub GetArrowString
    GoSub GetNoteLengFractions
    GoSub CombineFractions
    pAbcLeng = Fraction & Arrows & Tilde
    Return
    
'********************************************
    
GetLengNameFactor:
    ' Get the length of the pNoteLeng letter name
    ' as a factor of the default length.
    ' E.g if the default length is a quarter note
    ' from L:1/4, then a length of H is 2 of those,
    ' and a length of H3 is 6 of those quarter notes.
    TMName = Left$(pNoteLeng, 1) ' Q
    I = InStr(LengNames, TMName)
    Select Case pMeterLeng ' 4 if L:1/4 default length
        Case 1 ' "W" ' 1
            pAbcLeng = Wa(I)
        Case 2 ' H ' 2
            pAbcLeng = Ha(I)
        Case 4 ' Q ' 4
            pAbcLeng = Qa(I)
        Case 8 ' E ' 8
            pAbcLeng = Ea(I)
        Case 16 ' S ' 16
            pAbcLeng = Sa(I)
        Case 32 ' T ' 32
            pAbcLeng = Ta(I)
        Case Else
            pErr = "Time2Abc: Invalid note leng=" & _
                pNoteLeng & " or meter leng=" & _
                pMeterLeng
            pAbcLeng = ""
    End Select
    Return

'********************************************
    
' Get the numerator and denominator of the
' fraction looked up in Array.
' Blank = "1/1".  "/" = "1/2".
GetArrayFractions:
    Numer1 = 1
    Denom1 = 1
    If pAbcLeng = "" Then
        '
    ElseIf pAbcLeng = "/" Then
        Denom1 = 2
    Else ' N, N/, /D, N/D
        Numer1 = Val(pAbcLeng)
        I = InStr(pAbcLeng, "/")
        If I > 0 Then
            If Len(pAbcLeng) > I Then
                Denom1 = Val(Mid$(pAbcLeng, I + 1))
            End If
        End If
    End If
    If Numer1 < 1 Then Numer1 = 1
    If Denom1 < 1 Then Denom1 = 1 ' insurance
    Return
    
'********************************************
    
' Split the note leng string from the input cards
' after the initial letter,
' into the pAbcLeng part (integer or fraction)
' and the >>... or <<... dotted strings part.
GetArrowString:
    Numeric = Mid$(pNoteLeng, 2)
    Arrows = ""
    I = InStr(pNoteLeng, ">")
    If I > 0 Then
        Numeric = Mid$(pNoteLeng, 2, I - 2)
        Arrows = Mid$(pNoteLeng, I)
    End If
    I = InStr(pNoteLeng, "<")
    If I > 0 Then
        Numeric = Mid$(pNoteLeng, 2, I - 2)
        Arrows = Mid$(pNoteLeng, I)
    End If
    Return
    
'********************************************
    
' Get the numerator and denominator of the
' fraction passed in pNoteLeng.
' Blank = "1/1".  "/" = "1/2".
GetNoteLengFractions:
    Numer2 = 1
    Denom2 = 1
    If Numeric = "" Then
        '
    ElseIf Numeric = "/" Then
        Denom2 = 2
    Else ' N, N/, /D, N/D
        Numer2 = Val(Numeric)
        I = InStr(Numeric, "/")
        If I > 0 And Len(Numeric) > I Then
            Denom2 = Val(Mid$(Numeric, I + 1))
        End If
    End If
    If Numer2 < 1 Then Numer2 = 1
    If Denom2 < 1 Then Denom2 = 1 ' insurance
    Return
    
'********************************************
    ' The Format() function is like Str() but
    ' lacks a leading space.
CombineFractions:
    Numer3 = Numer1 * Numer2
    Denom3 = Denom1 * Denom2
    If Numer3 = Denom3 Then
        Fraction = ""
    ElseIf Numer3 = 1 And Denom3 = 2 Then
        Fraction = "/"
    ElseIf Denom3 = 1 Then
        Fraction = Format(Numer3)
    Else
        Gcd = fcnGCD(Numer3, Denom3)
        Numer3 = Numer3 / Gcd
        Denom3 = Denom3 / Gcd
        If Denom3 = 1 Then
            Fraction = Format(Numer3)
        Else
            Fraction = Format(Numer3) & _
                "/" & Format(Denom3)
        End If
    End If
    Return

End Sub

Private Sub SetNoteFlags(pFirstNote As Integer, _
                        pLastNote As Integer)
    ' called by sub cmdHarmony_Click
    ' calls function fcnNoteAfter
    ' calls function fcnGridNote

    ' Globals used: gNote(Note)...
    ' Globals set: gNote(x).bFirstChord,
            ' gNote(x).bPrevChord,
            ' gNote(x).bFinalChord

    Dim Note As Integer
    Dim Note2 As Integer
    Dim Voice As Integer
    Dim PriorChord As Integer
    Dim bNextIsFirstChord As Boolean
    Dim I As Integer
    Dim nRests As Integer
    Dim FirstChordNote As Integer

    ' .FirstChord - this note follows
    '               a DOUBLEBAR.
    ' It can be a .PrevChord but
    ' not a .FinalChord
    ' .FinalChord - this note is followed
    '               by a DOUBLEBAR.
    ' .PrevChord  - this note is followed
    '               by some chord note.
    ' This note and the following note
    ' are treated as a pair for testing
    ' for parallel fifths and for
    ' calculating the variance between them.

    ' If all four voices in a note are rests,
    ' then .bRestAll4=True.  Such a rest note
    ' is not a chord note and can occur anywhere,
    ' including next to a single or double bar.

    ' If all four voices have a rest at
    ' this note, then treat it as an
    ' end of phrase or period mark.
    ' Find the first note before it and mark
    ' it as a FinalChord.
    ' Find the first note after it and mark
    ' it as a FirstChord.

    ' TODO change GoTo Errn to GoSub Errn

'********************************************
'********************************************

Main:
    'Exit Sub
    GoSub Init
    GoSub MainLoop
    GoSub Conclusion
    Exit Sub

'********************************************
'********************************************

Init:
    If pFirstNote = 1 Then
        I = I ' testing
    End If
    PriorChord = 0
    bNextIsFirstChord = False
    For Note = pFirstNote To pLastNote
        With gNote(Note)
            .bBar = False
            .bChord = False
            '.bRestAll4 = False
            .bFirstChord = False
            .bPrevChord = False
            .bFinalChord = False
        End With
    Next Note
    Return

'********************************************

MainLoop:
    For Note = pFirstNote To pLastNote
        With gNote(Note)
            'GoSub Debug1
            GoSub Check4Rests
            'If .bRestAll4 Then ' treat as DoubleBar
            If .bRestAll4 Or .bRests3 Then ' treat as DoubleBar
            'If Note <> Note Then ' bypass
                bNextIsFirstChord = True
                If PriorChord > 0 Then
                    gNote(PriorChord).bFinalChord = True
                    gNote(PriorChord).bPrevChord = False
                    PriorChord = 0
                ElseIf FirstChordNote > 0 Then
                    'GoTo Err4 ' 30sep08
                End If
            ElseIf .Time = SINGLEBAR Then
                .bBar = True
            ElseIf .Time = DOUBLEBAR Then
                .bBar = True
                bNextIsFirstChord = True
                If PriorChord > 0 Then
                    gNote(PriorChord).bFinalChord = True
                    gNote(PriorChord).bPrevChord = False
                    PriorChord = 0
                'Else
                '    GoTo Err4
                End If
            Else ' is chord note, not a rest or bar
                gNote(Note).bChord = True
                If FirstChordNote = 0 Then
                    FirstChordNote = Note
                End If
                If bNextIsFirstChord Then
                    gNote(Note).bFirstChord = True
                    bNextIsFirstChord = False
                End If
                If PriorChord > 0 Then
                    gNote(PriorChord).bPrevChord = True
                End If
                PriorChord = Note
            End If
        End With
NextI:
    Next Note
    Return

'********************************************

Debug1:
    If gSongCurr = 3 _
    And fcnGridNote(Note) = 22 Then
        Note = Note
    End If
    If gSongCurr = 3 _
    And fcnGridNote(Note) = 23 Then
        Note = Note
    End If
    Return

'********************************************

Conclusion:
    If gNote(pLastNote).Time <> DOUBLEBAR Then
        GoTo Err3
    End If

    If gNote(pFirstNote).bBar Then
            GoTo Err5
    ElseIf gNote(pFirstNote).bRestAll4 Then
        Note2 = fcnNoteAfter(pFirstNote) ' call
        If Note2 > 0 Then
            gNote(Note2).bChord = True
            gNote(Note2).bFirstChord = True
        Else
            GoTo Err2
        End If
    Else
        gNote(pFirstNote).bChord = True
        gNote(pFirstNote).bFirstChord = True
    End If

    'Exit Sub
    Return

'********************************************

    ' Handle the case where all four voices
    ' have a rest in the first note of the
    ' song, that is, in an upbeat measure.
    ' 11/30/6. changed to any note.
Check4Rests:
    If gNote(Note).bRestAll4 Then
        Return
    End If
    nRests = 0
    For Voice = SOPRANO To BASS
        'With gNote(1).Voice(Voice)
        With gNote(Note).Voice(Voice)
        If .xRest And REST1 Then
            nRests = nRests + 1
        End If
        End With
    Next Voice
    If nRests >= 4 Then
        'gNote(1).bRestAll4 = True
        gNote(Note).bRestAll4 = True
    Else
        gNote(Note).bRestAll4 = False
    End If
    Return

'********************************************

Err2:
    Debug.Print "SetNoteFlags: no valid "; _
        "chord after the first note"; _
        pFirstNote; _
        "of TM: line in song="; _
        gSongCurr
    Stop: I = I ' testing
    Exit Sub

'********************************************

Err3:
    Debug.Print "SetNoteFlags: missing "; _
        "double bar || at lastnote="; _
        pLastNote - pFirstNote + 1; _
        " of TM: line in song="; _
        gSongCurr
    Stop: I = I ' testing
    Exit Sub

'********************************************

Err4:
    Debug.Print "SetNoteFlags: no valid "; _
        "chord before the double bar || "; _
        "or the Rest Note at "; _
        "note="; Note - pFirstNote + 1; _
        "of TM: line in song="; _
        gSongCurr
    I = I ' 1sep08
    'Stop
    Exit Sub

'********************************************

Err5:
    Debug.Print "SetNoteFlags: invalid "; _
        "single bar | or double bar || at "; _
        "firstnote="; pFirstNote; _
        "of TM: line in song="; _
        gSongCurr
    Stop: I = I ' testing
    Exit Sub

End Sub


Private Function fcnNoteBefore(pNote As Integer) _
                        As Integer
    ' (Called by sub SetNoteFlags)
    ' called by sub FinalTypeChord
    ' calls

    ' Compare with function fcnGetNoteBefore

    ' Globals used: gNote(pNote)...
    ' Globals set:

    ' Returns the number of first valid Note before
    ' pNote.  Return 0 if none found.
    Dim Note2 As Integer
    Dim Voice As Integer
    Dim FirstNote As Integer
    FirstNote = gSong(gSongCurr).FirstNote

    For Note2 = pNote - 1 To FirstNote Step -1
        If Not gNote(Note2).bBar _
        And Not gNote(Note2).bRestAll4 Then
            GoTo Exit1
        End If
    Next Note2
    Note2 = 0
Exit1:
    fcnNoteBefore = Note2

    End Function


Private Function fcnNoteAfter(pNote As Integer) _
                        As Integer
    ' called by sub SetNoteFlags
    ' called by sub cmdNextSolution_Click
    
    ' Globals used: gNote(pNote)...
    ' Globals set:

    ' Returns the number of first valid Note after
    ' pNote.  Return 0 if none found.
    Dim Note2 As Integer
    Dim FoundNote As Integer
    Dim LastNote As Integer
    LastNote = gSong(gSongCurr).LastNote
    FoundNote = 0
        
    For Note2 = pNote + 1 To LastNote
        With gNote(Note2)
            If Not .bBar And Not .bRestAll4 Then
                FoundNote = Note2
                Exit For
            End If
        End With
    Next Note2
    fcnNoteAfter = FoundNote
    End Function

Private Sub FinalTypeChord(pbFoundMatch As Boolean)
    ' called by sub Harmonize
    
    ' calls function fcnbCandChord
    ' calls sub ChooseLastChord
    ' calls sub DebugNote
    ' calls sub DebugBadCands
    ' calls sub DisplayNotes
    ' calls sub DisplayCandChords
    ' calls sub DisplayHarm
    
    ' Globals used: gNoteX, gNote(gNoteX)...,
    '       gKeyMode, gRomNum
    '       gScale()...
    ' Globals set: gRomNum =,
    
    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim bFinalRomNum(1 To TRIADALLOWEDMAX) As Boolean
    Dim InvNeeded(1 To TRIADALLOWEDMAX) As Integer
    Dim Dummy(1) As Integer

    gRomNum = gNote(gNoteX).RomNum
    ' If this is the last chord note in the song,
    ' either .LastNote or fcnNoteBefore(.LastNote),
    ' then set gRomNum = tonic.  Do not do this for
    ' any other FinalChord notes in the song,
    ' because it may be too much of a constraint.
    If gRomNum = NOTSET Then
        With gSong(gSongCurr)
            If gNoteX = .LastNote Then
                gRomNum = TONIC ' assume tonic chord
            ElseIf gNoteX = fcnNoteBefore(.LastNote) _
            Then
                gRomNum = TONIC ' assume tonic chord
            End If
        End With
    End If

    ' Scan all the chords of this scale deg
    ' to try to match the current (last) note
    ' with candidate chords from gChord().
    
    ' If the user specified a Roman numeral,
    ' drop the others.
    If gRomNum > 0 Then
        For K = 1 To TRIADALLOWEDMAX
            bFinalRomNum(K) = False
        Next K
        bFinalRomNum(gRomNum) = True
    Else
        For K = 1 To TRIADALLOWEDMAX
            bFinalRomNum(K) = True
        Next K
    End If

    ' Used by PrevTypeChord but should be init here:
    gScaleDegNeeded = NOTSET
    gInversionNeeded = NOINVERSIONNEEDED

    ' Dummy out this array which is used by
    ' sub PrevTypeChord:
    For K = 1 To TRIADALLOWEDMAX
        InvNeeded(K) = NOINVERSIONNEEDED
    Next K

    If fcnbCandChord(bFinalRomNum(), _
                    InvNeeded(), _
                    pbFoundMatch) Then ' call
        ' TODO CLEANUP
        'If Not pbFoundMatch Then
        '    I = I
        'End If
        Call ChooseLastChord(gNoteX) ' .BestCand
        If gNote(gNoteX).ChordInput <> 0 Then
            Call ChooseInputChord(gNoteX) ' .BestCand
        End If
        'call DebugNote(gNoteX)
        I = I
    Else
        'If gNote(gNoteX).BestCand <> 0 Then
            Debug.Print "FinalTypeChord:";
            Call DebugNote(gNoteX)
            Call DebugBadCands(gNoteX)
            I = I
            'gNote(gNoteX).BestCand = 0
        'End If
        GoTo Err1
    End If

    Exit Sub

Err1:
    gInputErrs = gInputErrs + 1
    K = gSong(gSongCurr).FirstNote
    Debug.Print "FinalTypeChord: no valid "; _
        "candidate chords for final note "; _
        " number="; gNoteX - K + 1; _
        "in songno="; gSongCurr
    GoSub DisplayChords
    'Stop
    K = K
    Exit Sub

DisplayChords:
    ' Display the candidates for
    ' this note with the new solution
    Call DisplayNotes(gSongCurr)
    Call DisplayCandChords 'gNoteX
    Call DisplayHarm(gSongCurr)
    gbShowHarmony = True
    cmdShow.Caption = _
        "&Show input"
    Return

' TODO not used:
Err2:
    gInputErrs = gInputErrs + 1
    Debug.Print "FinalTypeChord: no valid last note "; _
        "in songno="; gSongX
    Stop
Err3:
    gInputErrs = gInputErrs + 1
    Debug.Print "FinalTypeChord: less than one note "; _
        "in songno="; gSongX
    Stop
End Sub
Private Sub FirstTypeChord()
    ' called by sub Harmonize
    ' Globals used: gNoteX, gNote(gNoteX).RomNum
    ' Globals set: gRomNum =,

    'gRomNum = gNote(gNoteX).RomNum

    ' TODO 11/21/6
    ' This is probably nullified by sub PrevTypeChord.

    ' If this is the first note in the song,
    ' either .FirstNote or fcnNoteAfter(.FirstNote),
    ' then set gRomNum = tonic.  Do not do this for
    ' any other FirstChord notes in the song,
    ' because it may be too much of a constraint.
    If gNote(gNoteX).RomNum = NOTSET Then
        With gSong(gSongCurr)
            If gNoteX = .FirstNote Then
                gRomNum = TONIC ' assume tonic chord
            ElseIf gNoteX = fcnNoteAfter(.FirstNote) _
            Then
                gRomNum = TONIC ' assume tonic chord
            End If
        End With
    End If
End Sub

Private Sub PrevTypeChord(pbFoundMatch As Boolean)
    ' called by sub Harmonize

    ' calls sub ChordProgressions
    ' calls function fcnbCandChord
    ' calls sub CalcVariance
    ' calls sub ChoosePrevChord

    ' Globals used: gNoteX, gNote(gNoteX)...,
    '       gKeyMode, gRomNum
    '       gScale()...
    ' Globals set: gRomNum =, gAfterType =

    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim AfterCand As Long
    Dim bPrevRomNum(1 To TRIADALLOWEDMAX) As Boolean
    Dim InvNeeded(1 To TRIADALLOWEDMAX) As Integer

    'AfterCand = gHarm(gAfterX).BestCand ' best
    AfterCand = gNote(gAfterX).BestCand ' best
    ' candidate for this gNote(gAfterX) note.
    'If AfterCand > 0 Then
    '    I2 = gCand(AfterCand).Index2Ch
    If AfterCand = 0 Then
        I = I
    End If
    gxAfterFlag = gCand(AfterCand).xHarmProgFlag
    'gosub Debug1
    ' TODO Is V7 allowed or only V before iii6?
    ' I allow V7 in rootpos which should have
    ' the same bass as iii6.

    gScaleDegNeeded = NOTSET
    gInversionNeeded = NOINVERSIONNEEDED
    ' Must call ChordProgressions to set InvNeeded()
    ' for all notes, even if the user specifies
    ' a particular scale degree, e.g. V.
    ' GoSub Debug3
    'Debug.Print
    ' TODO
    If gNoteX = 1 Then
        I = I
    End If
    Call ChordProgressions(bPrevRomNum(), _
            InvNeeded())
    'GoSub Debug3
    ' TODO
    'If gRomNum = NOTSET Then
        gRomNum = gNote(gNoteX).RomNum
        ' .RomNum is only set by sub StoreRN.
        ' gRomNum is set by sub FirstTypeChord,
        ' but is nullified here. 11/21/6
    'End If
    If gRomNum = NOTSET Then ' scan all chords
       'GoSub Debug2
       ' If OP: ALLOW I V etc allows only certain
       ' triads, then turn off all the triads that
       ' are not allowed in the list.
        For K = 1 To TRIADALLOWEDMAX ' 7
            If bPrevRomNum(K) <> _
            gNote(gNoteX).bTriadAllowed(K) Then
            'gSong(gSongCurr).bTriadAllowed(K) then
                bPrevRomNum(K) = False
            End If
        Next K
    Else ' first/final chord for this scaledeg
        For K = 1 To TRIADALLOWEDMAX ' 7
            bPrevRomNum(K) = False
        Next K
        bPrevRomNum(gRomNum) = True
        'I = gScale(gKeyMode).FirstChord(gRomNum)
        'J = gScale(gKeyMode).FinalChord(gRomNum)
    End If
    'GoSub Debug3

    ' scan all the chords of this scale deg
    ' to try to match the current (last) note
    ' with candidate chords from gChord().
   'GoSub Debug1
    If fcnbCandChord(bPrevRomNum(), _
                    InvNeeded(), _
                    pbFoundMatch) Then ' call
        GoSub Debug4
        Call CalcVariance ' [gNoteX; gAfterX]
        Call ChoosePrevChord(gNoteX) ' .BestCand
        GoSub Debug4
        If gNote(gNoteX).ChordInput <> 0 Then
            Call ChooseInputChord(gNoteX) ' .BestCand
        End If
    Else
        GoSub Debug4
        Call CalcVariance ' [gNoteX; gAfterX]
    End If
    'GoSub Debug3
    Exit Sub

Debug4:
    If gSongCurr = 1 Then
        'If gNoteX = 34 Then
        If gNoteX = 1 Then
            I = I
        End If
    End If
    Return

Debug1:
    If gSongCurr = 1 _
    And gNoteX = fcnCol2Note(15) Then
        I = I ' dummy stop
    End If
    Return
    'And gNoteX = gSong(gSongCurr).FirstNote _
    '+ 1 Then

Debug2:
    ' TODO
    'If gSongCurr = 10 _
    'Or gSongCurr = 11 Then
    If gProgCol = 11 Then
         For K = 1 To TRIADALLOWEDMAX
             Debug.Print bPrevRomNum(K); InvNeeded(K);
         Next K
         Debug.Print
    End If
    'End If
    Return

Debug3:
    If gSongCurr = 1 Then
         Debug.Print "gNoteX; gRomNum="; _
            gNoteX; gRomNum; "  ";
         For K = 1 To TRIADALLOWEDMAX
            If bPrevRomNum(K) Then
                Debug.Print K;
            Else
                Debug.Print -K;
            End If
          Next K
         Debug.Print
    End If
    Return

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "PrevTypeChord: no valid last note "; _
        "in songno="; gSongX
    Stop
Err2:
    gInputErrs = gInputErrs + 1
    Debug.Print "PrevTypeChord: less than one note "; _
        "in songno="; gSongX
    Stop
End Sub


Private Function fcnCol2Note(pCol As Integer) _
                            As Integer
    ' called by sub PrevTypeChord
    ' called by user in debugging.
    fcnCol2Note = gSong(gSongCurr).FirstNote + _
        pCol - 1
End Function

Private Sub ChordProgressions(pbPrevRomNum() As Boolean, _
                            pInvNeeded() As Integer)
    ' called by sub PrevTypeChord
    ' calls function fcnErrAllow
    ' Globals used:
    ' Globals set: in sub InitProgressions;
    '    gbRomNumSave(1 To TRIADALLOWEDMAX)

    Dim I As Integer ' for testing
    Dim RowI As Integer
    Dim RowJ As Integer
    Dim Col As Integer
    Dim FirstNote As Integer
    Dim AfterCand As Long
    Dim AfterCx As Integer
    Dim A$
    Dim Cand As Long
    Dim E$
    Dim I9 As Integer
    Dim bRomNumNext(1 To TRIADALLOWEDMAX) As Boolean

    AfterCand = gNote(gAfterX).BestCand ' best
    ' candidate for this gNote(gAfterX) note.
    If AfterCand <= 0 Then
        'GoTo Err1
        GoSub Err1
        Exit Sub
    Else
        AfterCx = gCand(AfterCand).Index2Ch
    End If
    If AfterCx <= 0 Then
        'GoTo Err3
        GoSub Err3
        Exit Sub
    End If
    If gAfterX = 3 Then
        I = I
    End If

    Col = gChord(AfterCx).ScaleDeg
    ' iii can go to ii6 but not to ii (Ottman p 208)
    If Col = SUPERTONIC Then ' ii or II
        If gChord(AfterCx).Inversion = FIRSTINV _
        Then ' ii6 goes into col 8
            Col = 8
        End If
    ' If iii6, then only V-iii6 (Ottman p 305)
    ' If III6, then only V-III6 or v-III6.
    ElseIf Col = MEDIANT Then ' iii or III
        If gChord(AfterCx).Inversion = FIRSTINV _
        Then ' iii6 goes into col 10
            Col = 10
        End If
    ' v can go to iv6 but not to iv (Delaney
    ' CounterPoint Syllabus p 13)
    ElseIf Col = SUBDOMINANT Then ' IV or iv
        If gChord(AfterCx).Inversion = FIRSTINV _
        Then ' iv6 goes into col 11
            Col = 11
        End If
    ' If vi6, then only I-vi6 (Ottman p 305)
    ' If VI6, then only i-VI6 or I-VI6.
    ElseIf Col = SUBMEDIANT Then ' vi or VI
        If gChord(AfterCx).Inversion = FIRSTINV _
        Then ' vi6 goes into col 12
            Col = 12
        End If
    End If
    gProgCol = Col

    ' If the next two notes have scale degrees
    ' or iii-IV or iii-ii6,
    ' or III-iv or III-iio6,
    ' then use progression column 9.
    ' This allows 6-3-4 or 1-3 or 7-3 (in minor).
    If gCand(AfterCand).xHarmProgFlag _
         And HARMPROG_6_3_4 Then
             Col = 9
    End If

    ' If OP: ALLOW PRGANY, then allow all the
    ' bits that are turned on for the tonic I
    ' for this scale degree Col.

    A$ = fcnErrAllow(gNoteX, "PrgAny") ' call
    If Left$(A$, 1) = "(" Then
        RowJ = 1
        For RowI = TONIC To LEADINGTONE
            'RowJ = RowI
            GoSub CopyProgressions
        Next RowI
    Else
        For RowI = TONIC To LEADINGTONE
            RowJ = RowI
            GoSub CopyProgressions
        Next RowI
    End If
    ' TODO
    If Col = 6 Then
        'GoSub PrintRomNum
    End If
    GoSub AdjustScaleDegree1
    Exit Sub

CopyProgressions:
    Select Case gKeyMode
    Case MAJOR, DORIAN, PHRYGIAN, LYDIAN, MIXOLYDIAN
        pbPrevRomNum(RowI) = _
            gMajProgression(RowJ).bScaleDeg(Col)
        pInvNeeded(RowI) = _
            gMajProgression(RowJ).FromInversion(Col)
    Case MINORNAT
        pbPrevRomNum(RowI) = _
            gMinNatProg(RowJ).bScaleDeg(Col)
        pInvNeeded(RowI) = _
            gMinNatProg(RowJ).FromInversion(Col)
    Case MINORHARM
        pbPrevRomNum(RowI) = _
            gMinHarmProg(RowJ).bScaleDeg(Col)
        pInvNeeded(RowI) = _
            gMinHarmProg(RowJ).FromInversion(Col)
    Case MINORMELUP
        pbPrevRomNum(RowI) = _
            gMinMelUpProg(RowJ).bScaleDeg(Col)
        pInvNeeded(RowI) = _
            gMinMelUpProg(RowJ).FromInversion(Col)
    Case Else
        GoTo Err4
    End Select
    Return

    ' Per Ottman p 208 Table of Common Progressions,
    ' chords I,i :
    ' (1) may progress to any other triad
    ' (2) may interrupt any progression,
    '     such as ii-I-V
    ' I assume only one interruption is allowed.
    ' If the scale degree of the previous chord,
    ' Col, is 1, then merge the allowed progressions
    ' of the triad before that into it.
AdjustScaleDegree1:
    ' TODO cleanup
    'If gAfterX = 82 Then
       'GoSub PrintRomNum
       'GoSub PrintRomNumSave
        'I = I
    'End If
    ' Get the RomNum's that may progress to the scale
    ' degree of the note that follows the gAfterX,
    ' and merge it with the RomNum's of the scale degree
    ' of gAfterX.
    If Col = 1 Then ' scale degree 1
        'GoSub PrintRomNum
        Call ChordProgAfterNext(bRomNumNext())
        For RowI = TONIC To LEADINGTONE
            pbPrevRomNum(RowI) = pbPrevRomNum(RowI) _
                Or bRomNumNext(RowI)
        Next RowI
        'GoSub PrintRomNum
    End If

    ' TODO cleanup
    'If gAfterX = 82 Then
       'GoSub PrintRomNum
    '    I = I
    'End If
    Return

PrintRomNum:
    Debug.Print "Col, RN, IV=";
    For I9 = 1 To 7
        Debug.Print pbPrevRomNum(I9); pInvNeeded(I9);
    Next I9
    Debug.Print
    Return

Err1:
    gInputErrs = gInputErrs + 1
    FirstNote = gSong(gSongCurr).FirstNote
    E$ = "ChordProgressions: invalid AfterCand=" & _
        AfterCand & " for note gAfterX=" & _
        gAfterX - FirstNote + 1 & _
        " in song=" & gSongCurr
    'txtPrint.Text = E$
    Call DisplayText(E$)
    Debug.Print E$
    I = I
    Stop
    Return

Err3:
    gInputErrs = gInputErrs + 1
    FirstNote = gSong(gSongCurr).FirstNote
    E$ = "ChordProgressions: invalid AfterCx=" & _
        AfterCx & " for note gAfterX=" & _
        gAfterX - FirstNote + 1 & _
        " in song=" & gSongCurr
    'txtPrint.Text = E$
    Call DisplayText(E$)
    Debug.Print E$
    I = I
    Stop
    Return

Err4:
    gInputErrs = gInputErrs + 1
    FirstNote = gSong(gSongCurr).FirstNote
    E$ = "ChordProgressions: invalid gKeyMode=" & _
        gKeyMode & " in song=" & gSongCurr
    Call DisplayText(E$)
    Debug.Print E$
    I = I
    Stop
    'Return

' *************************
' not used
Err2:
    gInputErrs = gInputErrs + 1
    FirstNote = gSong(gSongCurr).FirstNote
    E$ = "ChordProgressions: invalid Cand=" & _
        Cand & "for note gNoteX=" & _
        gNoteX - FirstNote + 1 & _
        "in song=" & gSongCurr
    'txtPrint.Text = E$
    Call DisplayText(E$)
    Debug.Print E$
    I = I
    Stop
    Return

End Sub

Private Sub ChordProgAfterNext(pbRomNumNext() As Boolean)
    ' called by sub ChordProgressions
    ' Globals used:
    ' Globals set: in sub InitProgressions;

    ' Per Ottman p 208 TAble of Common Progressions,
    ' chords I,i :
    ' (1) may progress to any other triad
    ' (2) may interrupt any progression,
    '     such as ii-I-V
    ' I assume only one interruption is allowed.
    ' If the scale degree of the previous chord,
    ' K, is 1, then merge the allowed progressions
    ' of the triad before that into it.

    Dim I As Integer
    Dim K As Integer
    Dim FirstNote As Integer
    Dim LastNote As Integer
    Dim AfterNextCand As Long
    Dim AfterNextCx As Integer
    Dim E$
    Dim I9 As Integer

    LastNote = gSong(gSongCurr).LastNote
    For I = gAfterX + 1 To LastNote
        If gNote(I).BestCand > 0 Then
            AfterNextCand = gNote(I).BestCand
            Exit For
        End If
    Next I

    ' If at end of candidates, return zeros for the merge
    If AfterNextCand <= 0 Then
        For I = TONIC To LEADINGTONE
            pbRomNumNext(I) = False
        Next I
        Exit Sub
    End If

    AfterNextCx = gCand(AfterNextCand).Index2Ch
    If AfterNextCx <= 0 Then
        For I = TONIC To LEADINGTONE
            pbRomNumNext(I) = False
        Next I
        Exit Sub
    End If

    K = gChord(AfterNextCx).ScaleDeg
    If K = SUPERTONIC Then ' 2
        If gChord(AfterNextCx).Inversion = FIRSTINV _
        Then
            ' ii6 goes into col 8
            K = 8
        End If
    End If

    For I = TONIC To LEADINGTONE
        Select Case gKeyMode
        Case MAJOR, DORIAN, PHRYGIAN, LYDIAN, MIXOLYDIAN
            pbRomNumNext(I) = _
                gMajProgression(I).bScaleDeg(K)
        Case MINORNAT
            pbRomNumNext(I) = _
                gMinNatProg(I).bScaleDeg(K)
        Case MINORHARM
            pbRomNumNext(I) = _
                gMinHarmProg(I).bScaleDeg(K)
        Case MINORMELUP
            pbRomNumNext(I) = _
                gMinMelUpProg(I).bScaleDeg(K)
        Case Else
            GoTo Err4
        End Select
    Next I
    'GoSub PrintRomNumNext
    Exit Sub

PrintRomNumNext:
    Debug.Print "K="; K; "AN=";
    For I = 1 To 7
        Debug.Print pbRomNumNext(I);
    Next I
    Debug.Print
    Return

PrintRomNum:
    Debug.Print "K="; K; "RN=";
    For I9 = 1 To 7
        Debug.Print pbRomNumNext(I9);
    Next I9
    Debug.Print
    Return

Err4:
    gInputErrs = gInputErrs + 1
    FirstNote = gSong(gSongCurr).FirstNote ' TODO ???
    E$ = "ChordProgAfterNext: invalid gKeyMode=" & _
        gKeyMode & "in song=" & gSongCurr
    Call DisplayText(E$)
    Debug.Print E$
    I = I
    'Stop
End Sub

Private Function fcnbNextBest(pFirstNote As Integer, _
                    pLastNote As Integer, _
                    pBackNote As Integer) _
                    As Boolean
    ' (Called by sub cmdHarmony_Click)
    ' called by sub Harmonize

    ' Globals used: gSong()..., gNote(gNoteX)...
    ' Globals set: gNote(I).BestCand = 0, gNoteX =
    ' Const gMaxNextBest As Integer = 4

    Dim I As Integer
    Dim J As Integer
    Dim NumNext As Integer
    Dim E$

    ' If the first time for this error gNoteX,
    ' go up to
    '   gMaxNextBest (max is 4)
    ' gNotex's to the right of it and look for
    ' notes with more than one good candidate
    '   .nGoodCand > 1
    ' available, and use that as the
    ' working limit for backtracking.
    
    ' 11/21/6 I guess that, in this pass,
    ' we are looking for solutions with
    ' non-error notes only.
    
    ' gMaxNextBest = 3 was set by
    ' OP: MISC    MAXNEXT 3
    
    If gNoteX = 1 Then
        I = I
    End If
    NumNext = 0
    If gMaxNextBestNote = 0 Then
        For I = gNoteX + 1 To pLastNote
            With gNote(I)
                If .nGoodCand > 1 Then
                    If NumNext < gMaxNextBest Then
                        NumNext = NumNext + 1
                        gMaxNextBestNote = I
                    Else
                        Exit For
                    End If
                End If
            End With
        Next I
    End If

    ' skip over bars and rests and notes with
    ' only one candidate to the first note
    ' with more than one good candidate.
    fcnbNextBest = False
    pBackNote = 0 ' TODO probably not needed
    'For I = gNoteX + 1 To pLastNote
    For I = gNoteX + 1 To gMaxNextBestNote
        With gNote(I)
            If .nGoodCand > 1 Then
                gCand(.BestCand).Error = "NoSol0"
                If gbDebug Then
                    Debug.Print "fcnbNextBest: next:";
                    Call DebugNote(I)
                'Else
                '    GoSub Err1
                End If
                .nGoodCand = .nGoodCand - 1
                pBackNote = I
                If .bFinalChord Then
                    Call ChooseLastChord(pBackNote)
                Else
                    Call ChoosePrevChord(pBackNote)
                End If
                fcnbNextBest = True
                Exit Function
                
            ElseIf .nGoodCand = 1 Then
                gCand(.BestCand).Error = "NoSol1"
                If gbDebug Then
                    Debug.Print "fcnbNextBest: next:";
                    Call DebugNote(I)
                'Else
                '    GoSub Err1
                End If
                .nGoodCand = .nGoodCand - 1
            End If
        End With
    Next I
    
    If gbDebug Then
        Debug.Print "fcnbNextBest: exit loop, gMaxNextBestNote,I="; _
            gMaxNextBestNote; I
    End If
    
    For J = I To pLastNote
        If gNote(J).nGoodCand > 1 Then
            pBackNote = J
            Exit Function
        End If
    Next J
    Exit Function

'***************************************

Err1:
    E$ = "fcnbNextBest: no solution " _
        & "error=" & gCand(gNote(I).BestCand).Error _
        & " at note=" & I - pFirstNote + 1 _
        & " in song=" & gSongCurr
    Call DisplayText(E$)
    Debug.Print E$
    Return

End Function

Private Sub ChooseLastChord(pNoteX)
    ' called by sub FinalTypeChord
    ' called by sub cmdNextSolution_Click
    ' called by sub cmdPrevSolution_Click
    
    ' Globals used: pNoteX
    ' Globals set: gNote(pNoteX).BestCand =
    ' Choose best on candidate list.

    ' Globals used: pNoteX
    ' Globals set: gNote(pNoteX).BestCand =

    Const DUPE As Integer = 1
    Dim Cand As Long
    Dim KLong As Long
    Dim pA As Long
    Dim IA As Long
    Dim TRIPLE As Long
    Dim Other As Long
    Dim Chord As Integer ' pointer to gChord array
    Dim LeastRank As Integer
    Dim LeastIndex As Long
    Dim bBypass As Boolean
    Dim A$

    ' Choose best on candidate list.
    ' Determine the final chord type:
    '   Perfect Authentic, Imperfect Authentic, or
    '   Triple Root at final note (no fifth of chord).
    ' Assign a rank to each type.
    ' Choose the lowest rank found among
    ' the three chord types.

'*********************************************
'*********************************************
Main:
    GoSub Init
    GoSub MainLine
    GoSub Conclusion
    Exit Sub

'*********************************************
'*********************************************

Init:
    pA = 0 ' Perfect Authentic Cadence
    IA = 0 ' Imperfect Authentic Cadence
    TRIPLE = 0 ' Triple root chord at final note
    Other = 0
    LeastRank = 99
    LeastIndex = 99
    Return
    
'*********************************************

MainLine:
    With gNote(pNoteX)
        For Cand = .FirstCand To .LastCand
            If gCand(Cand).Error2 = "Final" Then
                '
            Else
                bBypass = False
                A$ = gCand(Cand).Error
                If A$ <> "" Then
                    ' I want to keep and show the
                    ' rank for "Soln", as documentation,
                    ' but not select it as next cand.
                    If Left$(A$, 3) = "Sol" Then
                        bBypass = True
                    ElseIf InStr(A$, "(") > 0 Then
                        ' add 20 to the rank below
                    Else
                        gCand(Cand).Rank = 0
                        GoTo NextCand
                    End If
                End If
            End If
            
            Chord = gCand(Cand).Index2Ch
            With gChord(Chord)
                'If .Voice(BASS).ChordPos _
                '= First Then
                If .DblType = DBLROOT Then
                    If .Voice(SOPRANO).ChordPos _
                    = FIRST Then
                        gCand(Cand).Rank = 1
                        If pA = 0 Then pA = Cand
                    Else
                        gCand(Cand).Rank = 4
                        If IA = 0 Then IA = Cand
                    End If
                ElseIf .DblType = FINALTRIPLE Then
                        gCand(Cand).Rank = 7
                        If TRIPLE = 0 Then
                            TRIPLE = Cand
                        End If
                Else
                    gCand(Cand).Rank = 10
                    If Other = 0 Then Other = Cand
                End If
                
                ' A chord with duplicate notes does
                ' not sound as full so add 1 to rank.
                If .bDupePitch Then
                    gCand(Cand).Rank = _
                        gCand(Cand).Rank + DUPE
                End If
                
                ' Boost the rank if the gap between
                ' Tenor and Bass in this note is equal or
                ' more than one or two octaves.
                ' This encourages smaller .BassGapCh's.
                ' Use 2 and 3 to account for DUPE=1.
                If .BassGapCh >= 24 Then
                    gCand(Cand).Rank = _
                        gCand(Cand).Rank + 3
                ElseIf .BassGapCh >= 12 Then
                    gCand(Cand).Rank = _
                        gCand(Cand).Rank + 2
                ElseIf .BassGapCh = 0 Then
                    gCand(Cand).Rank = _
                        gCand(Cand).Rank + 1
                End If
                
                ' Add 20 to rank of an allowed error
                ' to make it less likely to be selected.
                A$ = gCand(Cand).Error
                If InStr(A$, "(") > 0 Then
                    gCand(Cand).Rank = gCand(Cand).Rank _
                        + 20
                End If
                
                If gCand(Cand).Rank < LeastRank Then
                    If Not bBypass Then
                        LeastRank = gCand(Cand).Rank
                        LeastIndex = Cand
                    End If
                End If
            End With ' gChord
NextCand:
        Next Cand
    End With ' gNote
    Return
    
'*********************************************

Conclusion:
    'gHarm(pNoteX).BestCand = LeastIndex
    gNote(pNoteX).BestCand = LeastIndex
    If gNote(pNoteX).SolutionNo = 0 Then
        gNote(pNoteX).SolutionNo = 1
        KLong = gNote(pNoteX).BestCand
        gCand(KLong).PrevBest = 0
    End If
    'Exit Sub
    Return

'*********************************************

    ' TODO CLEANUP
    ' Not used any more:
    'With gHarm(pNoteX)
    With gNote(pNoteX)
        If pA > 0 Then
            .BestCand = pA
        ElseIf IA > 0 Then
            .BestCand = IA
        ElseIf TRIPLE > 0 Then
            .BestCand = TRIPLE
        Else
            .BestCand = Other
        End If
    End With

    'If gSongCurr = 1 _
    'And pNoteX = gSong(gSongCurr).FirstNote _
    '+ 2 Then
        'call PrintTest(pNoteX)
    'End If
End Sub
Private Sub ChooseInputChord(pNoteX)
    ' called by Sub FinalTypeChord
    ' called by Sub PrevTypeChord
    
    ' Globals used: pNoteX
    ' Globals set: gNote(pNoteX).BestCand =
    
    ' This Sub is called if all four voices were
    ' input for this gNoteX.
    ' If the four voices matched those of some chord,
    ' use the candidate that points to that chord,
    '   .InputCand
    ' as the .BestCand. Else, use .BestCand=0.

    Dim K As Integer
    ' Choose best on candidate list.
    
    With gNote(pNoteX)
        If .ChordInput > 0 Then
            .BestCand = .InputCand
        ElseIf .ChordInput < 0 Then
            .BestCand = 0
        End If
        
        If gNote(pNoteX).SolutionNo = 0 Then
            gNote(pNoteX).SolutionNo = 1
            K = gNote(pNoteX).BestCand
            If K > 0 Then
                gCand(K).PrevBest = 0
            End If
        End If
    End With
    'Exit Sub
    End Sub
Private Sub SaveRestoreNextSong(pFlag As String)
    ' called by Sub cmdNextSong_click
    ' called by Sub cmdPrevSong_click
    
    ' calls sub cmdNextSolution_Click
    ' calls sub cmdPrevNote_Click
    
    ' Globals used: gSongCurr,
    '               gNote(pNoteX).BestCand
    ' Globals set: gNote(pNoteX).BestCandIndex =,
    
    ' If parm pFlag = "save" then save the index
    ' to the .BestCand for each note in this song
    ' before going on to the next song.
    
    ' If parm pFlag = "restore" then restore the index
    ' to the .BestCand for each note in this song
    ' from the last time that this song was displayed.
    
    ' Otherwise, the program wll display the .BestCand
    ' for each note that was calculated initially.
    ' This can be annoying because the user has to
    ' make his changes to the panel manually whenever
    ' he returns to a song.
    
    ' More importantly, if you want to write out all the
    ' songs to the ABC program as one song, you want
    ' each song to keep your manual choices.
    ' Note that the program recalculates and displays
    ' each song anew before it can write them all out
    ' as one song.
    
    Dim I As Integer
    Dim J As Integer
    Dim Fn As Integer
    Dim Ln As Integer
    Dim Note As Integer
    Dim Index1 As Integer
    Dim SaveIndex As Integer
    Dim bUserCand As Boolean
    Dim Bc As Integer
    Dim Uc As Integer
    Dim SolSave As Integer
    Dim E$
    Dim MyCandAdj As Integer
    Dim MyCand As Integer
  
'*******************************************
'*******************************************

MainLine:
    bUserCand = False
    If pFlag = "save" Then
        GoSub SaveBestCands
    ElseIf pFlag = "restore" Then
        GoSub RestoreBestCands
    Else
        Debug.Print "SaveRestoreNextSong: " & _
            "pFlag must be ""save"" or ""restore"""
        I = I
        Stop
    End If
    Exit Sub
    
'*******************************************
'*******************************************

' Before going on to the Next or Prev song, save the
' .BestCand for each note in the current gSongCurr.
SaveBestCands:
    'If gSongCurr = 2 Then
    '    I = I
        'Stop
    'End If
    Fn = gSong(gSongCurr).FirstNote
    Ln = gSong(gSongCurr).LastNote
    'GoSub Debug1
    For Note = Ln To Fn Step -1
        With gNote(Note)
            If .bChord Then
                If .BestCand >= 0 Then
                    .BestCandIndex = _
                        gCand(.BestCand).Index2Ch
                Else
                    I = I
                    Stop
                End If
                .SolutionNoSave = .SolutionNo
            ElseIf Not (.bBar Or .bRestAll4) Then
                I = I
                Stop
            End If
        End With
    Next Note
    'GoSub Debug1
    'Debug.Print
    Return

'*******************************************

' After going to the next song, for each note
' keep on invoking cmdNextSolution_Click
' until .BestCand matches .BestCandIndex.
' This restores the screen for this song back
' to where it was last left.
RestoreBestCands:
    'If gSongCurr = 2 Then
    '    I = I
        'Stop
    'End If
    Fn = gSong(gSongCurr).FirstNote
    Ln = gSong(gSongCurr).LastNote
    'Debug.Print
    'GoSub Debug1
    ' Note that after sub NextSong,
    ' the screen shows the candidates for
    ' the last note in the song.
    For Note = Ln To Fn Step -1
        With gNote(Note)
            If .bChord Then
                If .BestCandIndex > 0 Then
                    GoSub RestoreBestCand
                End If
                ' Since the "Soln" displayed on the
                ' panel is used in calculations, you
                ' must switch to the previous note
                ' and display its candidates on the screen
                ' before you restore the .BestCand for it.
                Call cmdPrevNote_Click
            ElseIf Not (.bBar Or .bRestAll4) Then
                I = I ' insurance
                Stop
            End If
        End With
Next1:
    Next Note
    'GoSub Debug1
    'Debug.Print
    Return
    
'*******************************************

' After NextSong, each note has its initial
' .BestCand.  Keep getting the next solution
' until its index matches the .BestCandIndex
' that was saved by GoSub SaveBestCands.
' But if .SolutionNo matches .SolutionNoSave,
' then look for a UserCand candidate that was
' input by the MyCand text box.

RestoreBestCand:
    
    With gNote(Note)
        Index1 = gCand(.BestCand).Index2Ch
        Do While Index1 <> .BestCandIndex
            SaveIndex = Index1
            Call cmdNextSolution_Click
            ' got a new .BestCand and .SolutionNo
            Index1 = gCand(.BestCand).Index2Ch
            If .SolutionNo = .SolutionNoSave Then
                Exit Do
            End If
            ' insurance.  stop if index1 doesn't change
            If SaveIndex = Index1 Then
                I = I
                Exit Do
            End If
        Loop
        If Index1 <> .BestCandIndex Then
            GoSub RestoreUserCand
        End If
    End With
    Return

'*******************************************

' If you have repeated NextSolution until
' .SolutionNoSave was reached without
' a match between .BestCand and .BestCandIndex,
' then use .UserCand as the new .BestCand.

RestoreUserCand:
    With gNote(Note)
        If .UserCand <= 0 Then
            bUserCand = False
            GoSub Err1
        'ElseIf Left$(gCand(.UserCand).Error, 1) <> _
        '"(" Then
        '    bUserCand = False
        '    GoSub Err1
        Else
            ' reverse this calculation from sub
            ' MyCandidate to get the number
            ' typed by the user in MyCand text box.
            '   MyCandAdj = .FirstCand - 1 + _
            '        Abs(MyCand)
            MyCandAdj = .UserCand
            MyCand = MyCandAdj - .FirstCand + 1
            txtMyCand.Text = Str(MyCand)
            Call MyCandidate
            bUserCand = True
        End If
    End With
    Return
    
'*******************************************

Debug1:
    Debug.Print pFlag
    For I = Fn To Ln
        J = gNote(I).BestCand
        Debug.Print gSongCurr; " "; I; _
            gCand(J).Index2Ch; gNote(I).BestCandIndex
    Next I
    Return

'*******************************************

Err1:
    Bc = gNote(Note).BestCand
    Uc = gNote(Note).UserCand
    SolSave = gNote(Note).SolutionNoSave
    E$ = "SaveRestoreNextSong: can't restore " & _
        "note=" & gNoteX & " in song=" & gSongCurr & _
        ", invalid Bc Uc or Solno=" & Bc & Uc & SolSave
    Call DisplayText(E$)
    Debug.Print E$
    GoSub Debug1
    I = I
    Stop
    Return

End Sub

Private Sub ChoosePrevChord(pNoteX As Integer)
    ' called by sub PrevTypeChord
    ' called by sub fcnbNextBest
    ' called by sub cmdNextSolution_Click
    ' called by sub cmdPrevSolution_Click
    
    ' calls function fcnGridNote
    
    ' Globals used: pNoteX
    ' Globals set: gNote(pNoteX).BestCand =

    ' TODO 11/25/6. I changed I to Cand.

    ' Find the candidate with either
    ' (1) the lowest rank within the lowest variance
    ' or
    ' (1) the lowest variance within the lowest rank
    ' and return it in
    '   gNote(pNoteX).BestCand = LowIndex
    
    ' Handle the candidates with Contrary motion
    ' before any others.  If none, then those
    ' with Oblique motion, then the rest.
    
    ' TODO 12/1/6. In sub ChoosePrevChord,
    ' clean up Option 3, where I was looking
    ' for the highest variance for Contrary or
    ' Oblique cand's.
    
    Const DUPE As Integer = 1
    Dim Cand As Long
    Dim KLong As Long
    Dim Ch As Integer
    Dim A$
    Dim E$
    Dim Diff
    Dim bByte As Byte
    Const LOWVAL As Byte = 255 ' all 1's

    ' Used in GoSub VarianceOverRank
    Dim LowIndex As Long
    Dim LowVariance As Single
    Dim LowRank As Integer
    Dim LowRank1 As Integer
    Dim LowRank2 As Integer
    Dim LowChord As Integer

    ' Used in GoSub OuterVariance
    Dim HighIndex As Long
    'Dim HighVariance As Single
    
    Dim HighIndex1 As Long
    Dim HighVariance1 As Single
    Dim HighRank1 As Integer
    Dim HighChord1 As Integer
    
    Dim HighIndex2 As Long
    Dim HighVariance2 As Single
    Dim HighRank2 As Integer
    Dim HighChord2 As Integer
    
    Dim bContrary As Boolean
    Dim bOblique As Boolean
    Dim bExitVar As Boolean

'*********************************************
'*********************************************
Main:
    GoSub Init
    GoSub MainLine
    GoSub Conclusion
    Exit Sub

'*********************************************
'*********************************************

Init:
    GoSub Debug5
    Return

'*********************************************
Debug5:
    If gSongCurr = 1 Then
        If fcnGridNote(gNoteX) >= 3 _
        And fcnGridNote(gNoteX) <= 3 Then
            gNoteX = gNoteX
        End If
    End If
    Return

'********************************************

    ' If an OP: ALLOW xxx line was entered,
    ' then error xxx is allowed because
    ' fcnErrAllow will prefix it with '(',
    ' e.g. "(xxx".
    ' If the error=Sol, just display the rank.
    ' Else if blank or (error, use the rank to calc.
MainLine:
    With gNote(pNoteX)
        If .nGoodCand <= 0 Then
            Stop
        End If
        For Cand = .FirstCand To .LastCand
            A$ = gCand(Cand).Error
            If A$ = "" Then
                GoSub CalcChordRank
            Else
                If Left$(A$, 3) = "Sol" _
                Or InStr(A$, "(") > 0 Then
                    GoSub CalcChordRank
                Else
                    gCand(Cand).Rank = 0
                End If
            End If
        Next Cand
    End With
    
    'GoSub Debug4
    ' If there were CONTRARY or OBLIQUE cand's,
    ' try to get the cand with the lowest variance
    ' or rank
    ' from them first, then try the rest, which
    ' are SIMILAR or STATIONARY. 03/08/08
    If gNote(pNoteX).NumContraryOblique > 0 Then
        'GoSub OuterVariance
        If gSong(gSongCurr).xMotionRequest _
        And CONTRARY Then
            bByte = CONTRARY
            GoSub Over
            If LowIndex > 0 Then
                Return
            End If
        End If
        If gSong(gSongCurr).xMotionRequest _
        And OBLIQUE Then
            bByte = OBLIQUE
            GoSub Over
            If LowIndex > 0 Then
                Return
            End If
        End If
        bByte = LOWVAL
        GoSub Over
    Else ' try all types
        bByte = LOWVAL
        GoSub Over
    End If
    Return

Over:
    'bByte = LOWVAL '
    If gbRankOverVariance Then
        GoSub RankOverVariance
    Else
        GoSub VarianceOverRank
    End If
    Return

Debug4:
    If gSongCurr = 1 _
    And fcnGridNote(pNoteX) = 3 Then
        Debug.Print "ChoosePrevChord:D4"; _
            pNoteX; gNote(pNoteX).nGoodCand; _
            Cand; gCand(Cand).Error
        Cand = Cand
    End If
    Return
            
            ' TODO 11/26/6 not used
            If A$ <> "" Then
                If Left$(A$, 3) = "Sol" Then
                    '
                ElseIf InStr(A$, "(") > 0 Then
                    '
                Else
                    gCand(Cand).Rank = 0
                    'GoTo Next1
                End If
            End If
            GoSub CalcChordRank

'*********************************************

Conclusion:
    If HighIndex > 0 Then
        gNote(pNoteX).BestCand = HighIndex
        If gNote(pNoteX).SolutionNo = 0 Then
            gNote(pNoteX).SolutionNo = 1
            KLong = HighIndex
            gCand(KLong).PrevBest = 0
        End If
    ElseIf LowIndex > 0 Then
        gNote(pNoteX).BestCand = LowIndex
        If gNote(pNoteX).SolutionNo = 0 Then
            gNote(pNoteX).SolutionNo = 1
            KLong = LowIndex
            gCand(KLong).PrevBest = 0
        End If
    Else
        KLong = KLong
        GoSub Debug3
        'Stop 3/13/08
    End If
    Return
        
'*********************************************

' TODO 11/26/6. Should this Rank be calculated and
' stored in the Chord, like the Variance is stored
' in the Cand?  The Variance is computed on two
' notes, but the Rank is computed on one chord.
CalcChordRank:
    Ch = gCand(Cand).Index2Ch
    With gChord(Ch)
        If .Inversion = ROOTPOS Then
            If .DblType = DBLROOT Then
                gCand(Cand).Rank = 1
            Else
                gCand(Cand).Rank = 7
            End If
        ElseIf .Inversion = FIRSTINV Then
            If .DblType = DBLPRIMSOP Then
                gCand(Cand).Rank = 1
            ElseIf DBLSOPRANO Or DBLPRIMARY Then
                gCand(Cand).Rank = 4
            Else
                gCand(Cand).Rank = 7
            End If
        ' TODO 11/25/6 why do these have Rank=1 ?
        ' Probable answer is that, if not,
        ' we would never see a 3-note
        ' 6 4 pattern, e.g. I I64 I, which is
        ' not often formed.
        ' TODO maybe instead I should check for
        ' specific 6-4 cases.
        ElseIf .Inversion = SECONDINV Then
            gCand(Cand).Rank = 1
        ElseIf .Inversion = THIRDINV Then
            gCand(Cand).Rank = 1
        End If
        
        ' A chord with duplicate notes does
        ' not sound as full so add 1 to rank.
        If .bDupePitch Then
            gCand(Cand).Rank = _
                gCand(Cand).Rank + DUPE
        End If
        
        ' Boost the rank if the gap between
        ' Tenor and Bass in this note is
        ' more than one or two octaves.
        ' This encourages smaller BassGapCh's.
        ' Use 2 and 3 to account for DUPE=1.
        If .BassGapCh >= 24 Then
            gCand(Cand).Rank = _
                gCand(Cand).Rank + 3
        ElseIf .BassGapCh >= 12 Then
            gCand(Cand).Rank = _
                gCand(Cand).Rank + 2
        ElseIf .BassGapCh = 0 Then
            gCand(Cand).Rank = _
                gCand(Cand).Rank + 1
        End If
        
        ' Add 20 to rank of an allowed error
        ' to make it less likely to be selected.
        A$ = gCand(Cand).Error
        If InStr(A$, "(") > 0 Then
            gCand(Cand).Rank = gCand(Cand).Rank _
                + 20
        End If
            
    ' If this cand for gNoteX has the same scale
    ' degree step as gAfterX, e.g. V-V or v-V,
    ' flag it as a .bDupe in sub CalcVariance.
    ' If the variance between them is less than
    '   gMinVarianceForDupes
    ' then add this cand's rank to 20 to discourage
    ' but not prevent the boring use of near dupe
    ' chords, especially when the program creates
    ' 3 or 4 voices 18mar08
        If gCand(Cand).bDupe Then
            If gCand(Cand).Variance >= 20! Then
                If gCand(Cand).Rank < 20 Then
                    gCand(Cand).Rank = gCand(Cand).Rank + 20
                End If
            End If
        End If
    End With ' gChord
    Return

'*********************************************

    ' OPTION 1:
    ' Choose the error-free candidate
    ' with the lowest (best) Variance.
    ' If the same Variance, choose the note with
    ' the lower (better) Rank.
    ' Rank=0,1,4,7 or
    ' Rank=0,2,5,8 if dupe
VarianceOverRank:
    LowIndex = 0
    LowVariance = 9999! ' single variable
    LowRank = 99
    With gNote(pNoteX)
        If .nGoodCand <= 0 Then
            Stop
        End If
        'LowIndex = .FirstCand
        'LowVariance = gCand(.FirstCand).Variance
        'LowRank = gCand(.FirstCand).Rank
        'For Cand = .FirstCand + 1 To .LastCand
        For Cand = .FirstCand To .LastCand
            If (gCand(Cand).xOuterMotion And bByte) = 0 Then
                GoTo NextVar
            End If
            A$ = gCand(Cand).Error
            If Left$(A$, 3) = "Sol" Then
                GoTo NextVar
            ElseIf fcnErr(gCand(Cand).Error) = "" Then
                If gCand(Cand).Variance >= 0! Then
                    GoSub LowestVariance
                Else ' insurance
                    gCand(Cand).Error = "Var<0"
                    Stop
                End If
                If .nGoodCand = 1 Then ' no use going on
                    Exit For
                End If
            End If
NextVar:
        Next Cand
    End With
    Return

'*********************************************

' If this candidate's Variance is the lowest
' so far, choose it.  If it is equal to the
' lowest Variance so far, compare the Ranks.
' OP: MISC VarianceOverRank RankOper=LE VarRound=.5
LowestVariance:
    If gCand(Cand).Variance > 0! _
    And gCand(Cand).Variance < LowVariance Then '3/8/08
        LowVariance = gCand(Cand).Variance
        LowIndex = Cand
        LowRank = gCand(Cand).Rank ' reset rank
        LowChord = gCand(Cand).Index2Ch ' doc only
    Else
        'Stop ' 12/1/6 testing logic
        ' treat variances as equal if within gVarRound
        Diff = Abs(gCand(Cand).Variance - LowVariance)
        If Diff <= gVarRound Then
            GoSub TheLowerRank
            'GoSub Debug0 ' uncommon?
        End If
    End If
    'GoSub Debug0
    Return
    
        ' TODO not used 11/26/6
    If gCand(Cand).Variance < LowVariance Then
        LowVariance = gCand(Cand).Variance
        LowIndex = Cand
        LowRank = gCand(Cand).Rank
    ElseIf gCand(Cand).Variance = LowVariance Then
        If gCand(Cand).Rank >= 0 Then
            'GoSub LowRank
        Else ' insurance
            gCand(Cand).Error = "Rank<0"
        End If
    End If

'*********************************************

' If the Rank for this Variance is the lowest
' so far, choose it.  Else if it is equal to
' the lowest so far, choose it if gbRankLE is
' True by option
TheLowerRank:
    If gCand(Cand).Rank < 0 Then ' insurance
        gCand(Cand).Error = "Rank<0"
        Stop
    Else
        If gbRankLE Then
            If gCand(Cand).Rank <= LowRank Then
                LowIndex = Cand
                LowRank = gCand(Cand).Rank
            End If
        Else ' gbRankLT
            If gCand(Cand).Rank < LowRank Then
                LowIndex = Cand
                LowRank = gCand(Cand).Rank
            End If
        End If
    End If
    Return

'*********************************************

    ' OPTION 2:
    ' Choose the error-free candidate
    ' with the lowest (best) Rank.
    ' If the same Rank, choose the note with
    ' the lower (better) Variance.
    ' Rank=0,1,4,7 or
    ' Rank=0,2,5,8 if dupe
RankOverVariance:
    LowIndex = 0
    LowRank = 99
    LowVariance = 0! ' string variable
    With gNote(pNoteX)
        If .nGoodCand <= 0 Then Stop
        For Cand = .FirstCand To .LastCand
            If (gCand(Cand).xOuterMotion And bByte) = 0 Then
                GoTo NextRank
            End If
            A$ = gCand(Cand).Error
            If Left$(A$, 3) = "Sol" Then
                GoTo NextRank
            ElseIf fcnErr(gCand(Cand).Error) = "" Then
                If gCand(Cand).Rank >= 0 Then
                    GoSub LowestRank
                Else ' insurance
                    gCand(Cand).Error = "Rank<0"
                    Stop
                End If
                If .nGoodCand = 1 Then ' no use going on
                    Exit For
                End If
            End If
NextRank:
        Next Cand
    End With

    Return

'*********************************************

' If this candidate's Rank is the lowest
' so far, choose it.  If it is equal to the
' lowest Rank so far, compare the Variances.
' If the Variance for this Rank is the lowest
' so far, choose it.  Else if it is equal to
' the lowest so far, choose it if gbVarianceLE is
' True by option
' OP: MISC RankOverVariance VarOper=LE
' VarRound=.5 is not used or accepted.
LowestRank:
    If gCand(Cand).Rank < LowRank Then
        LowRank = gCand(Cand).Rank
        LowIndex = Cand
        LowVariance = gCand(Cand).Variance ' reset
    ElseIf gCand(Cand).Rank = LowRank Then
        'Stop ' 12/1/6 testing logic
        If gCand(Cand).Variance >= 0 Then
            GoSub TheLowerVariance
        Else ' insurance
            gCand(Cand).Error = "Var<0"
            Stop
        End If
    End If
    Return

'*********************************************

TheLowerVariance:
    If gbVarianceLE Then
        If gCand(Cand).Variance > 0 _
        And gCand(Cand).Variance <= LowVariance Then
            LowIndex = Cand
            LowVariance = gCand(Cand).Variance
        End If
    Else ' gVarianceLT
        If gCand(Cand).Variance > 0 _
        And gCand(Cand).Variance < LowVariance Then
            LowIndex = Cand
            LowVariance = gCand(Cand).Variance
        End If
    End If
    Return
        
'*********************************************

    ' OPTION 3:
    ' Choose the error-free
    ' Contrary or Oblique candidate
    ' with the highest (best) Variance,
    ' the is, the most convergence or divergence.
    ' If the same Variance, choose the note with
    ' the lower (better) Rank.
    ' Calculate separately for both Contrary
    ' and Oblique candidates, then choose.
OuterVariance:
    HighIndex = 0
    'HighVariance = 0! ' string variable
    'LowRank = 99
    HighIndex1 = 0
    HighVariance1 = 0# ' string variable
    LowRank1 = 99
    HighIndex2 = 0
    HighVariance2 = 0# ' string variable
    LowRank2 = 99
    ' find cand's with Contrary or Oblique motion
    With gNote(pNoteX)
        If .nGoodCand <= 0 Then Stop
        For Cand = .FirstCand To .LastCand
            If (gCand(Cand).xOuterMotion And CONTRARY) Then
                bContrary = True
                bOblique = False
                GoSub CalcOuterVariance
                If bExitVar Then Exit For
            ElseIf (gCand(Cand).xOuterMotion And OBLIQUE) Then
                bOblique = True
                bContrary = False
                GoSub CalcOuterVariance
                If bExitVar Then Exit For
            End If
        Next Cand
    End With
    'GoSub Debug1
    'GoSub Debug2
    If gSongCurr = 2 _
    And fcnGridNote(pNoteX) = 7 Then
        pNoteX = pNoteX
    End If

    If HighIndex1 > 0 Then
        HighIndex = HighIndex1
    Else
        HighIndex = HighIndex2
    End If
    Return

'*********************************************

CalcOuterVariance:
    A$ = gCand(Cand).Error
    If Left$(A$, 3) = "Sol" Then
        '
    ElseIf fcnErr(gCand(Cand).Error) = "" Then
        If gCand(Cand).Variance >= 0 Then
            If gSongCurr = 1 _
            And fcnGridNote(pNoteX) = 3 Then
                pNoteX = pNoteX
            End If
            If bContrary Then
                GoSub HighestVariance1
            ElseIf bOblique Then
                GoSub HighestVariance2
            End If
            If gNote(pNoteX).nGoodCand = 1 Then
                bExitVar = True
                'Exit For ' no use going on
            Else
                bExitVar = False
            End If
        Else ' insurance
            gCand(Cand).Error = "Var<0"
            Stop
        End If
    End If
    Return

'*********************************************

' If this candidate's Variance is the Highest
' so far, choose it.  If it is equal to the
' Highest Variance so far, compare the Ranks.
' OP: MISC VarianceOverRank RankOper=LE VarRound=.5
' Contrary's
HighestVariance1:
    If gCand(Cand).Variance > HighVariance1 Then
        HighVariance1 = gCand(Cand).Variance
        HighIndex1 = Cand
        LowRank1 = gCand(Cand).Rank ' reset rank
        HighChord1 = gCand(Cand).Index2Ch ' doc only
    Else
        ' treat variances as equal if within gVarRound
        Diff = Abs(gCand(Cand).Variance - HighVariance1)
        If Diff <= gVarRound Then
            GoSub TheLowerRank1
        End If
    End If
    Return
        
' Oblique's
HighestVariance2:
    If gCand(Cand).Variance > HighVariance2 Then
        HighVariance2 = gCand(Cand).Variance
        HighIndex2 = Cand
        LowRank2 = gCand(Cand).Rank ' reset rank
        HighChord2 = gCand(Cand).Index2Ch ' doc only
    Else
        ' treat variances as equal if within gVarRound
        Diff = Abs(gCand(Cand).Variance - HighVariance2)
        If Diff <= gVarRound Then
            GoSub TheLowerRank2
        End If
    End If
    Return
        
'*********************************************

' If the Rank for this Variance is the lowest
' so far, choose it.  Else if it is equal to
' the lowest so far, choose it if gbRankLE is
' True by option
' Contrary's
TheLowerRank1:
    If gCand(Cand).Rank < 0 Then ' insurance
        gCand(Cand).Error = "Rank<0"
        Stop
    Else
        If gbRankLE Then
            If gCand(Cand).Rank <= LowRank1 Then
                HighIndex1 = Cand
                LowRank1 = gCand(Cand).Rank
                HighChord1 = gCand(Cand).Index2Ch
            End If
        Else ' gbRankLT
            If gCand(Cand).Rank < LowRank1 Then
                HighIndex1 = Cand
                LowRank1 = gCand(Cand).Rank
                HighChord1 = gCand(Cand).Index2Ch
            End If
        End If
    End If
    Return

' Oblique's
TheLowerRank2:
    If gCand(Cand).Rank < 0 Then ' insurance
        gCand(Cand).Error = "Rank<0"
        Stop
    Else
        If gbRankLE Then
            If gCand(Cand).Rank <= LowRank2 Then
                HighIndex2 = Cand
                LowRank2 = gCand(Cand).Rank
                HighChord2 = gCand(Cand).Index2Ch
            End If
        Else ' gbRankLT
            If gCand(Cand).Rank < LowRank2 Then
                HighIndex2 = Cand
                LowRank2 = gCand(Cand).Rank
                HighChord2 = gCand(Cand).Index2Ch
            End If
        End If
    End If
    Return

Debug1:
    E$ = "ChoosePrevChord:D1 gSongCurr,gNoteX=" _
        & gSongCurr & "," _
        & fcnGridNote(pNoteX) _
        & " Contrary Chord Hi idx var rank=" _
        & HighChord1 & "," _
        & HighIndex1 & "," & HighVariance1 & "," _
        & HighRank1
    Call DisplayText(E$)
    Debug.Print E$
    Return
        '& pnoteX - gSong(gSongCurr).FirstNote + 1

Debug2:
    E$ = "ChoosePrevChord:D2 gSongCurr,gNoteX=" _
        & gSongCurr & "," _
        & fcnGridNote(pNoteX) _
        & "  Oblique Chord Hi idx var rank=" _
        & HighChord2 & "," _
        & HighIndex2 & "," & HighVariance2 & "," _
        & HighRank2
    Call DisplayText(E$)
    Debug.Print E$
    Return

Debug0:
    E$ = "ChoosePrevChord:D0 gSongCurr,gNoteX=" _
        & gSongCurr & "," _
        & fcnGridNote(pNoteX) _
        & " Contrary Chord low idx var rank=" _
        & LowChord & "," _
        & LowIndex & "," & LowVariance & "," _
        & LowRank
    Call DisplayText(E$)
    Debug.Print E$
    Return
        '& pnoteX - gSong(gSongCurr).FirstNote + 1

Debug3:
    E$ = "ChoosePrevChord:D3 gSongCurr,gNoteX=" _
        & gSongCurr & "," _
        & fcnGridNote(pNoteX) _
        & " ????? Contrary Chord low idx var rank=" _
        & LowChord & "," _
        & LowIndex & "," & LowVariance & "," _
        & LowRank
    Call DisplayText(E$)
    Debug.Print E$
    Return
        '& pnoteX - gSong(gSongCurr).FirstNote + 1

End Sub

Private Sub Check_6_3_4(pCandLast As Long)
'Private Sub Check_6_3_4(pCandLast As Integer)
    ' (Called by sub Harmonize)
    ' called by sub fcnbCandChord
    ' Globals used:
    ' Globals set:
    ' After each candidate is made, fcnbCandChord
    ' calls this Sub Check_6_3_4 to set the
    ' HARMPROG bit for handling Six-Three-Four chords.

    ' If flag HARMPROG_6_3_4 is set to true
    ' and if the current candidate is vi or VI, then
    ' chord progression should be vi-iii-IV for
    ' major key or VI-III-iv for minor key.
    ' Ottman pg 207 2.

    ' The HARMPROG_6_3_4 flag allows the harmonic
    ' progression vi-iii-IV or VI-III-iv but does
    ' not force it when iii-IV or III-iv occurs.
    ' If vi-iii or VI-III, then IV or iv must
    ' (usually) follow.

    ' Ottman pg 304 can substitute ii6 for IV or
    ' iio6 for iv after mediant because both
    ' progress to V and they have the same Bass.
    ' Major: vi-iii-IV or vi-iii-ii6
    ' Minor: VI-III-iv or VI-III-iio6

    ' vi6 and iii6 is infreqent.  See I-vi6, V-iii6.
    ' vi64 and iii64 is rare.  Ottman p 305.
    ' So I check for ROOTPOS.

    ' TODO should I allow iv6 or IV6 in
    ' the progression (but not vi6 or iii6)?

    'Dim Cand1 As Integer
    Dim Cand1 As Long
    Dim Chord1 As Integer
    'Dim Cand2 As Integer
    Dim Cand2 As Long
    Dim Chord2 As Integer

    Cand1 = pCandLast
    Chord1 = gCand(Cand1).Index2Ch
    Cand2 = gNote(gAfterX).BestCand
    Chord2 = gCand(Cand2).Index2Ch

    ' Case: pCandLast chord's scaledeg
    ' is    iii in vi-iii-IV
    ' or    iii in vi-iii-ii6

    If gChord(Chord2).ScaleDeg = SUBDOMINANT _
    And gChord(Chord2).Inversion = ROOTPOS Then
        GoSub CheckMediant
    ElseIf gChord(Chord2).ScaleDeg = SUPERTONIC _
    And gChord(Chord2).Inversion = FIRSTINV Then
        GoSub CheckMediant
    End If


    Exit Sub

CheckMediant:
    'If gNote(gNoteX).RomNum = MEDIANT _
    'And gNote(gNoteX).Inversion = ROOTPOS Then
    If gChord(Chord1).ScaleDeg = MEDIANT _
    And gChord(Chord1).Inversion = ROOTPOS Then
        gCand(Cand1).xHarmProgFlag = _
            gCand(Cand1).xHarmProgFlag _
            Or HARMPROG_6_3_4
    End If
    Return

End Sub


Private Sub Check_6_4(pCandLast As Long)
'Private Sub Check_6_4(pCandLast As Integer)
    ' called by sub fcnbCandChord
    ' Globals used:
    ' Globals set:
    ' After each candidate is made, fcnbCandChord
    ' calls this Sub Check_6_4 to set the
    ' HARMPROG bits for handling Six-Four chords.

    Dim I As Integer
    Dim bErr As Boolean
    'Dim Cand1 As Integer
    Dim Cand1 As Long
    Dim Chord1 As Integer
    Dim Index1 As Integer

    'Dim Cand2 As Integer
    Dim Cand2 As Long
    Dim Chord2 As Integer
    Dim Index2 As Integer

'***********************************************
'***********************************************

Main:
    GoSub Init
    GoSub CheckForRests
    If bErr Then
        Exit Sub
    End If
    GoSub CheckV_I
        'gosub TurnOffBitsForInsurance
    If gChord(Chord1).Inversion = SECONDINV Then
        GoSub CheckI64_V_I
            'gosub TurnOffBitsForInsurance
        GoSub Check6_4_Pedal
            'gosub TurnOffBitsForInsurance
        GoSub Check6_4_Passing
            'gosub TurnOffBitsForInsurance
    End If
    Exit Sub

'***********************************************
'***********************************************

Init:
    bErr = False
    Cand1 = pCandLast
    Chord1 = gCand(Cand1).Index2Ch
    Cand2 = gNote(gAfterX).BestCand
    Chord2 = gCand(Cand2).Index2Ch
    Return

'***********************************************

CheckForRests:
    If gNote(gNoteX).bFinalChord Then
        bErr = True
    ElseIf gAfterX > gNoteX + 1 Then
        For I = gNoteX + 1 To gAfterX - 1
            If gNote(gNoteX).bRestAll4 Then
                bErr = True
                Exit For
            End If
        Next I
    End If
    Return

'***********************************************

CheckV_I:
    'If gNoteX = 43 Then
    '    Cand1 = Cand1 ' testing
    'End If
    ' Case 1: Cadential Six-Four Part 1
    ' Case 1: pCandLast is V in V-I
    ' Prepare for a possible I64_V_I.
    ' Accepts V7 for V, i or i6 for I, and
    ' i64 for I64.
    If gChord(Chord2).ScaleDeg = TONIC Then
        If gChord(Chord1).ScaleDeg = DOMINANT Then
            If gChord(Chord1).Inversion = ROOTPOS Then
                GoSub TurnOffBitsForInsurance
                gCand(Cand1).xHarmProgFlag = _
                    gCand(Cand1).xHarmProgFlag _
                    Or HARMPROG_V_I ' set bit=1
                Exit Sub
            End If
        End If
    End If
    Return

'***********************************************

    'If gChord(Chord1).Inversion <> SECONDINV Then
    '    Exit Sub
    'End If

CheckI64_V_I:
    'Cand2 = gNote(gAfterX).BestCand
    'Chord2 = gCand(Cand2).Index2Ch
    'Index2 = gChord(Chord2).Voice(BASS).Index2Na

    ' Case 1: Cadential Six-Four Part 2
    ' Case 1: pCandLast is I64 in I64-V-I
    ' Must have already checked for 6-5
    ' and 4-3 above the bass.
    ' Usually the bass notes are the same.

    If gCand(Cand2).xHarmProgFlag _
    And HARMPROG_V_I Then
        'If gNote(gNoteX).RomNum = TONIC Then
        If gChord(Chord1).ScaleDeg = TONIC Then
            GoSub TurnOffBitsForInsurance
            gCand(Cand1).xHarmProgFlag = _
                gCand(Cand1).xHarmProgFlag _
                Or HARMPROG_I64_V_I ' set bit=1
        Exit Sub
        End If
    End If
    Return

'***********************************************

Check6_4_Pedal:
    Index1 = gChord(Chord1).Voice(BASS).Index2Na

    'Cand2 = gNote(gAfterX).BestCand
    Chord2 = gCand(Cand2).Index2Ch
    Index2 = gChord(Chord2).Voice(BASS).Index2Na

    ' Case 2 Pedal Six-Four
    ' a) bass notes are equal (or on octave?);
    ' Must have already checked the following:
    ' b) notes above the bass usually move stepwise
    '   (this should be taken care of by normal
    '   4Part smoothing).

    ' TODO it may be possible in music theory to
    ' allow some other voice, like the soprano,
    ' to hold the pedal note along with a 6-4.

    If Index1 = Index2 Then
        GoSub TurnOffBitsForInsurance
        gCand(Cand1).xHarmProgFlag = _
            gCand(Cand1).xHarmProgFlag _
            Or HARMPROG_6_4_Pedal ' bass notes =
        Exit Sub
    End If
    Return

'***********************************************

Check6_4_Passing:
    ' Case 3 Passing Six-Four
    ' a) three bass notes move stepwise in a straight
    '    line up or down;
    ' Must have already checked the following:
    ' b) the 4th (tonic) above the bass is usually
    '    stationary.  A Bach exception like Ottman p 177
    '    can be permitted by the gbSmooth option.
    ' c) the other notes above the bass notes usually
    '    move stepwise in a straight line.
    '    line up or down
   'If Abs(Index1 - Index2) Mod 12 <= 2 Then

    If Abs(Index1 - Index2) <= 2 Then
        GoSub TurnOffBitsForInsurance
        gCand(Cand1).xHarmProgFlag = _
            gCand(Cand1).xHarmProgFlag _
            Or HARMPROG_6_4_Passing ' stepwise notes
        If Index1 < Index2 Then
            gCand(Cand1).xHarmProgFlag = _
                gCand(Cand1).xHarmProgFlag _
                Or HARMPROG_6_4_RisingBass ' set bit=1
        Else
            gCand(Cand1).xHarmProgFlag = _
                gCand(Cand1).xHarmProgFlag _
                And Not HARMPROG_6_4_RisingBass ' set bit=0
        End If
    End If
    Return
    'Exit Sub

'***********************************************

TurnOffBitsForInsurance:
        gCand(Cand1).xHarmProgFlag = _
            gCand(Cand1).xHarmProgFlag _
            And Not HARMPROG_6_4_Pedal ' set bit=0

        gCand(Cand1).xHarmProgFlag = _
            gCand(Cand1).xHarmProgFlag _
            And Not HARMPROG_6_4_Passing ' set bit=0
        gCand(Cand1).xHarmProgFlag = _
            gCand(Cand1).xHarmProgFlag _
            And Not HARMPROG_6_4_RisingBass ' set bit=0

        gCand(Cand1).xHarmProgFlag = _
            gCand(Cand1).xHarmProgFlag _
            And Not HARMPROG_V_I ' set bit=0
        gCand(Cand1).xHarmProgFlag = _
            gCand(Cand1).xHarmProgFlag _
            And Not HARMPROG_I64_V_I ' set bit=0
    Return

End Sub


Private Function fcnBassStep(pCh As Integer) _
                            As Integer
    ' (Called by sub Harmonize)
    ' called by sub fcnCand2ndInv
    ' calls:
    ' Globals used: gAfterX
    ' Globals set:

    ' Find the difference, Diff, between the indexes
    ' of the the Bass notes of the input pCh and
    ' gAfterX. If abs(Diff) > 2 (a whole step),
    ' then return error value 9. Else return 1
    ' if gAfterX.Bass rises, 0 if the same,
    ' and -1 if it falls. E.g. if G1 in the bass
    ' for pCh rises to A1 in the bass for gAfterX,
    ' then return 1.

    ' This fcn is used mostly  to see if the bass
    ' notes before and after a Six-Four chord are
    ' the same or moving step-wise.

    Dim Index1 As Integer

    Dim Cand2 As Long
    Dim Chord2 As Integer
    Dim Index2 As Integer
    Dim Diff As Integer
    Dim E$
    Dim FirstNote As Integer

    Index1 = gChord(pCh).Voice(BASS).Index2Na

    Cand2 = gNote(gAfterX).BestCand
    Chord2 = gCand(Cand2).Index2Ch
    Index2 = gChord(Chord2).Voice(BASS).Index2Na

    If Index2 <= 0 Then
        GoSub Err1
    Else
        Diff = Index2 - Index1
        If Diff = 0 Then
            fcnBassStep = 0
        Else
        If Abs(Diff) > 2 Then
            fcnBassStep = 9 ' more than a whole step
        Else
        ' If number is          Sgn returns
        '   Greater than zero    1
        '   Equal to zero        0
        '   Less than zero      -1
            fcnBassStep = Sgn(Diff)
        End If
    End If
    Exit Function

Err1:
    FirstNote = gSong(gSongCurr).FirstNote ' TODO ???
    E$ = "fcnBassStep: invalid gAfterX=" & _
        gAfterX & " for gChord=" & pCh & _
        " in song=" & gSongCurr
    Call DisplayText(E$)
    Debug.Print E$
    Stop
    Return

    End If
End Function

Private Sub DisplayCandChords()
    ' called by sub Harmonize
    ' called by sub cmdNextNote_Click
    ' called by sub cmdPrevNote_Click
    ' called by sub cmdValidCand_Click

    ' calls sub ClearExtra
    ' calls sub SetGridExtraLabels
    ' calls sub cmdChords_Click

    ' Globals used: gNoteX, gNote(gNoteX)...,
    '       gNote(gNoteX).BestCand
    ' Globals set: grdExtra.Cols =,
    '       txtNoCands.Text =, txtBest.Text =,
    '       txtGoodCand.text=

    Dim Chord As Integer
    Dim Cand As Long
    Dim Voice As Integer
    Dim Voice1 As Integer
    Dim Voice2 As Integer
    Dim GridCols As Integer
    Dim bNoteFound As Boolean
    Dim K As Integer
    Dim N As Integer

    Dim A As Integer
    Dim C As Integer
    Dim V As Integer
    Dim B$
    Dim E$
    Dim Incr As Integer
    Dim bExit As Boolean
'Dim gDNoteVoice$ ' display in this voice
'Dim gDNoteRequest$ ' display chords with this note eg #B3

'*********************************************
'*********************************************

Main:
    GoSub Init
    If Not bExit Then
        GoSub MainLine
    End If
    Exit Sub
    
'*********************************************
'*********************************************

Init:
    If gNoteX <= 0 _
    Or gNoteX > gNoteLast Then
        GoSub Err1
        bExit = True
    ElseIf gNote(gNoteX).LastCand <= 0 Then
        GoSub Err1
        bExit = True
    End If
    Return
    
'*********************************************

MainLine:
    GoSub GridRequest
    GoSub GridSetup
    GoSub GridMisc
    GoSub GridRows
    Return

'*********************************************

' Count the numbers of cols that contain the note
' name gDNoteRequest$, e.g. #B4, in the voice(s)
' gDNoteVoice$=S, A, T, B, or (blank or ALL),
' in order to initialize the lower grid, grdExtra.
' This is a way to filter out cols that do not
' that note, for debugging a song.
GridRequest:
    If gDNoteRequest$ <> "" Then
        GoSub GridRequestCount
        If GridCols > 1 Then
            grdExtra.Cols = GridCols + 1
        Else
            grdExtra.Cols = 2
        End If
        Call ClearExtra ' clear grid to blanks
        grdExtra.FillStyle = grdSingle ' single cells
        Call SetGridExtraLabels ' reset col num's
    End If
    Return

'*********************************************

GridRequestCount:
    GridCols = 1
    Select Case UCase$(gDNoteVoice$)
        Case "S"
            Voice1 = 1
            Voice2 = 1
        Case "A"
            Voice1 = 2
            Voice2 = 2
        Case "T"
            Voice1 = 3
            Voice2 = 3
        Case "B"
            Voice1 = 4
            Voice2 = 4
        Case "", "ALL"
            Voice1 = 1 ' soprano
            Voice2 = 4 ' bass
        Case Else
            Voice1 = 1
            Voice2 = 4
    End Select
    GridCols = 0
    With gNote(gNoteX)
        'call cmdChords_Click ' see if chords change
        For Cand = .FirstCand To .LastCand
            Chord = gCand(Cand).Index2Ch
            GoSub GridRows1to4
            If bNoteFound Then
                GridCols = GridCols + 1
            End If
        Next Cand
    End With
    Return
    
'*********************************************

GridRows1to4:
    bNoteFound = False
    For Voice = Voice1 To Voice2
        K = gChord(Chord).Voice(Voice).Index2Na
        B$ = gNoteName(K)
        If B$ = gDNoteRequest$ Then
            bNoteFound = True
            Exit For
        End If
    Next Voice
    Return

'*********************************************

GridSetup:
    With gNote(gNoteX)
        If gDNoteRequest$ <> "" Then
            '
        ElseIf gbShowValidCands Then
            'grdExtra.Cols = .nGoodCand + 1
            ' display "Soln" as valid cand too:
            If .nGoodCand + .SolutionNo < 1 Then
                grdExtra.Cols = 2
            Else
                grdExtra.Cols = .nGoodCand + .SolutionNo
                If grdExtra.Cols < 2 Then
                    grdExtra.Cols = 2 ' 05may08
                End If
            End If
        Else
            grdExtra.Cols = .LastCand - _
                .FirstCand + 2
        End If
        
        Call ClearExtra ' clear grid to blanks
        grdExtra.FillStyle = grdSingle ' single cells
        Call SetGridExtraLabels ' reset col num's
    End With
    Return

'*********************************************

GridMisc:
    With gNote(gNoteX)
        txtMeas.Text = Str$(.MeasureNo)
        'Debug.Print gSongCurr; gNoteX; _
            gNote(gNoteX).MeasureNo
        N = gNoteX - gSong(gSongCurr).FirstNote
        txtNoteNo.Text = Str$(N + 1)
        
        If gDNoteRequest$ <> "" Then
            txtNoCands.Text = grdExtra.Cols - 1 ' 3/13/08
        Else
            txtNoCands.Text = .LastCand - .FirstCand + 1
        End If
        'If gHarm(gNoteX).BestCand > 0 Then
        If gNote(gNoteX).BestCand > 0 Then
            'txtBest.Text = gHarm(gNoteX).BestCand
            txtBest.Text = gNote(gNoteX).BestCand _
                - gNote(gNoteX).FirstCand + 1
        Else
            txtBest.Text = ""
        End If
        txtGoodCand.Text = .nGoodCand
        txtCandSolNo = .SolutionNo
    End With
    Return

'*********************************************

GridRows:
    With gNote(gNoteX)
        gSongKey = .SongKey
        ' TODO why is this needed here?
        Call cmdChords_Click ' see if chords change
        grdExtra.Col = 0
        For Cand = .FirstCand To .LastCand
            Chord = gCand(Cand).Index2Ch
            If gDNoteRequest$ <> "" Then
                GoSub GridRows1to4
                If Not bNoteFound Then
                    GoTo NextCand
                End If
            ElseIf gbShowValidCands Then
                B$ = gCand(Cand).Error
                If B$ <> "" Then
                    If Left$(B$, 3) <> "Sol" Then
                        GoTo NextCand
                    End If
                End If
            End If
            grdExtra.Col = grdExtra.Col + 1
            grdExtra.Row = 0
            grdExtra.Text = Cand - .FirstCand + 1

            GoSub Rows1to4
            GoSub ROW5

            grdExtra.Row = 6
            If .bAlterAboveBass Then
                grdExtra.Text = .AlterAboveBassNM _
                    & "," & gChord(Chord).FBName
            ElseIf .bAlterSDeg Then
                grdExtra.Text = .AlterSDegNM _
                    & "," & gChord(Chord).FBName
            Else
                grdExtra.Text = gChord(Chord).FBName
            End If
            
            ' Warn that the figured bass inversion
            ' may be wrong in case the Bass is
            ' Crossed with other note. 3aug08
            If gCand(Cand).Error = "(Cross" Then
                If gNote(gNoteX).bCrossedBass Then
                    grdExtra.Text = grdExtra.Text & "?"
                End If
            End If

            grdExtra.Row = 7
            grdExtra.Text = _
                Str$(gCand(Cand).Variance)

            grdExtra.Row = 8
            grdExtra.Text = gCand(Cand).Error

            If gCand(Cand).Rank > 0 Then
                grdExtra.Row = 9
                grdExtra.Text = gCand(Cand).Rank
            End If
            
            grdExtra.Row = 10
            grdExtra.Text = gCand(Cand).OuterGraphic _
                & "     " & gCand(Cand).InnerGraphic

            grdExtra.Row = 11
            grdExtra.Text = Chord ' index to cand's chord
        
NextCand:
        Next Cand
    End With
    Return

'*********************************************

Rows1to4:
    For Voice = SOPRANO To BASS ' 1 to 4
        grdExtra.Row = Voice
        K = gChord(Chord).Voice(Voice).Index2Na
        B$ = gNoteName(K)
        grdExtra.Text = fcnHideKeyAccidentals(B$)
    Next Voice
    Return
        
'*********************************************

ROW5:
    grdExtra.Row = 5
    'grdExtra.Text = gChord(Chord).ScaleDeg
    grdExtra.Text = _
        gChord(Chord).RomNumName
    If gCand(Cand).VSlash <> "" Then
        grdExtra.Text = grdExtra.Text & "=" & _
            gCand(Cand).VSlash
    End If
    Return
    
'*********************************************

Err1:
    E$ = "DisplayCandChords:E1 " _
    & "invalid gNoteX=" & gNoteX
    Call DisplayText(E$)
    Debug.Print E$
    Stop
    Return

'*********************************************

Err2:
    E$ = "DisplayCandChords:E2 " _
    & "no candidates for gNoteX=" & gNoteX
    Call DisplayText(E$)
    Debug.Print E$
    Stop
    Return
    
End Sub
Private Sub ClearExtra()
    ' called by sub DisplayCandChords
    ' Globals used:
    ' Globals set:

    ' Note that all the cells fill with dark blue.
    ' When you click in any cell, all the cells
    ' turn to white.  When you click and drag,
    ' you turn the selected cells to dark blue.

    ' Select from Row #1, Col #1
    grdExtra.SelStartCol = 1
    grdExtra.SelStartRow = 1
    ' End selection at bottom right cell
    grdExtra.SelEndCol = grdExtra.Cols - 1
    grdExtra.SelEndRow = grdExtra.Rows - 1

    ' Set FillStyle to 1 (fill them all).
    grdExtra.FillStyle = grdRepeat ' 1

    ' Fill all the cells with null.
    grdExtra.Text = " "

End Sub



Private Function fcnbAlteredFB(pNoteX As Integer, _
                pChord As Integer) _
                As Boolean
    ' called by sub fcnbCandChord

    ' Globals used: gNote(gNoteX).AlterAboveBass(n)
    '   gChord(pCh).Voice(CrdVoice).ChordPos
    '   gChord(pCh).Inversion
    '   gScaleDegree
    ' Globals set:

    ' Enter the OffsetTbl table
    '   with pTriadno=1,3,5,7
    '   and pInversion=0,1,2
    
    ' NOTE if you do not specify the bass note e.g. G4
    ' in the input, then you must use #^3 to raise
    ' the third above the root of the scale,
    ' whatever it is.
    ' If you specify the specify the inversion but
    ' not the bass note, then you can use
    ' e.g. 5 3 #3.
    ' You can also use RN: v FB: #3 to get V.

    Static OffsetTbl(1 To 7, 0 To 3)
    Static bOffset As Boolean
    Dim I1 As Variant
    Dim I3 As Variant
    Dim I5 As Variant
    Dim I7 As Variant
    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim TriadNo As Integer
    Dim Inver As Integer
    Dim AboveBass As Integer
    Dim CrdVoice As Integer
    Dim Idx As Integer

    Dim IndexNm As Integer
    Dim ScaleDegree As Integer

    ' NEW triad table:
    '                   5  1
    '           3  1    3  6
    '  1  1     1  6    1  4
    '  5  5     5 (3)   5 (1)
    '  3  3     3 (1)
    '  1 (1)

    ' cp = triad chord pos; fb figured bass
    ' triad table:
    '                   3  6
    '           1  6    1  4
    '  5  5     5 (3)   5 (1)
    '  3  3     3 (1)
    '  1 (1)

    ' cp fb    cp fb   cp fb
    'rootpos  1stinv  2ndinv

    ' seventh chord table:
    '                           5 (6)
    '                   3 (6)   3  4
    '           1  6    1  4    1  2
    '  7  7     7  5    7  3    7 (1)
    '  5 (5)    5 (3)   5 (1)  [5 (6)]
    '  3 (3)    3 (1)  [3 (6)] [3  4]
    '  1 (1)   [1  6]  [1  4]  [1  2]

    ' cp fb    cp fb   cp fb   cp fb
    'rootpos  1stinv  2ndinv  3rdinv

'*********************************************
'*********************************************
Main:
    GoSub Init
    GoSub MainLine
    Exit Function

'*********************************************
'*********************************************

Init:
    If gNoteX = 1 Then
        I = I
    End If
    fcnbAlteredFB = False
    If Not bOffset Then ' init STATIC variables only once
        bOffset = True
        ' Taken from the right (fb) col of each
        ' pair of the bottom four rows of
        ' the seventh chord table above.
        I7 = Array(7, 5, 3, 1) ' variants
        I5 = Array(5, 3, 1, 6) '   used to
        I3 = Array(3, 1, 6, 4) '   init
        I1 = Array(1, 6, 4, 2) '   OffsetTbl(,)
        For J = 0 To 3
            OffsetTbl(7, J) = I7(J)
            OffsetTbl(6, J) = 0
            OffsetTbl(5, J) = I5(J)
            OffsetTbl(4, J) = 0
            OffsetTbl(3, J) = I3(J)
            OffsetTbl(2, J) = 0
            OffsetTbl(1, J) = I1(J)
        Next J
    End If
    Return
    
'*********************************************
    
    ' TODO cannot return both: AboveBass or SDeg
    ' If the same note (tonic) has #3 and #^3
    ' referring to the same pitch, then the
    ' SDeg path is chosen over the AboveBass.
    ' The results should be the same, but the
    ' SDeg is more precise in case of chords with
    ' different bass notes, e.g. when bass is calc'd.
MainLine:
    If gNote(pNoteX).bAlterSDeg Then
        GoSub SDeg
    ElseIf gNote(pNoteX).bAlterAboveBass Then
        GoSub AboveBass
    End If
    Return

'*********************************************

AboveBass:
    ' Given the triad number 1,3,5,7 and the
    ' inversion 0,1,2, the OffsetTbl table
    ' returns the figured bass number,
    ' that is, the chord number (1-7) above
    ' the bass note.
    ' The .Alter array was set in sub StoreFB
    ' with the increment +1 or decrement
    ' -1 or no change 0 to add to the
    ' chord index value, corresponding to the
    ' figured bass #3 or b3 or #6 or b6, etc.
    ' E.g. if the Figured Bass subfield was b6,
    ' then subfield .AlterAboveBass(6) contains incr=-1.
    ' E.g. if the Figured Bass subfield was #3,
    ' then subfield .AlterAboveBass(3) contains incr=+1.
    With gChord(pChord)
    For CrdVoice = SOPRANO To BASS
        Idx = .Voice(CrdVoice).Index2Na
        If gNeverDouble(Idx) > 0 Then
            TriadNo = .Voice(CrdVoice).ChordPos
            Inver = .Inversion
            K = OffsetTbl(TriadNo, Inver)
            AboveBass = gNote(pNoteX).AlterAboveBass(K)
            ' TODO cleanup. May be needed in sub fcnbParallel:
            If AboveBass <> 0 Then
                fcnbAlteredFB = True
                If TriadNo = THIRD Then
                    gNote(pNoteX).ThirdIncr = AboveBass
                End If
                ' TODO
                'If Idx = 46 Then
                '    GoSub Debug1
                '    I = I
                'End If
            End If
        End If
    Next CrdVoice
    End With
    Return

'*********************************************
    
    ' The gScaleDegree array is set in sub
    ' InitScaleDegree.  In a particular scale, e.g.
    ' C Major, the tonic is scale degree 1 and the
    ' Leading Tone is scale degree 7.
    ' Given the index of a voice of a chord,
    ' get the scale degree for that note in the array.
    ' Then get the increment, -1 or +1 or 0,  at that
    ' scale degree.
    ' TODO a doubled note should not be altered.
    ' e.g. if the tonic is doubled, then it should
    ' not be raised by #^1.
    ' Index2Na is index into gNoteName array.
SDeg:
    With gChord(pChord)
    For CrdVoice = SOPRANO To BASS
        Idx = .Voice(CrdVoice).Index2Na
        If gNeverDouble(Idx) > 0 Then
            ScaleDegree = gScaleDegree(Idx)
            AboveBass = gNote(pNoteX).AlterSDeg(ScaleDegree)
            ' TODO cleanup. May be needed in sub fcnbParallel:
            If AboveBass <> 0 Then
                fcnbAlteredFB = True
                TriadNo = .Voice(CrdVoice).ChordPos
                If TriadNo = THIRD Then
                    gNote(pNoteX).ThirdIncr = AboveBass
                End If
                ' TODO
                'If Idx = 46 Or Idx = 34 Then
                '    GoSub Debug1
                '    I = I
                'End If
            End If
        End If
    Next CrdVoice
    End With
    Return

'*********************************************
' TODO not used
SDeg9:
    IndexNm = gChord(pChord).Voice(CrdVoice).Index2Na
    ScaleDegree = gScaleDegree(IndexNm)
    AboveBass = gNote(pNoteX).AlterSDeg(ScaleDegree)
    ' TODO cleanup. May be needed in sub fcnbParallel:
    If AboveBass <> 0 Then
        TriadNo = gChord(pChord).Voice(CrdVoice).ChordPos
        If TriadNo = THIRD Then
            gNote(pNoteX).ThirdIncr = AboveBass
        End If
    End If
    Return

'*********************************************

Debug1:
    With gChord(pChord)
        For I = SOPRANO To BASS
            J = .Voice(I).Index2Na
            Debug.Print gNoteName(J); _
                .Voice(I).ChordPos;
        Next I
        Debug.Print "idx="; Idx
    End With
    Return

End Function

Private Function fcnGCD(ByVal pA As Integer, _
                ByVal pB As Integer) _
                As Integer
    
    ' called by sub Time2ABC
    
    ' Globals used:
    ' Globals set:

    ' Enter with pA and pB, non-zero integers.
    ' Return with their Greatest Common Divisor
    ' or GCD.  That is, reduce a fraction to
    ' lowest terms.
    ' E.g. the GCD of 6/4 is 2, so 6/4=3/2 * 2/2.
    
    ' This algorithm was taken from the article
    ' on the Euclidean algorithm in Wikipedia.

    Dim T As Integer

     While pB <> 0
         T = pB
         pB = pA Mod pB
         pA = T
    Wend
    fcnGCD = pA

End Function




Private Function fcnHideKeyAccidentals( _
                pN As String) _
                As String

    ' called by sub DisplayNotes
    ' called by sub DisplayCandChords
    ' called by sub DisplayHarm
    ' Uses globals:
    '   gSong(gSongCurr).KeySigUsedIst
    '   gSong(gSongCurr).KeySigIst()
    
    Dim I As Integer
    Dim K As Integer
    Dim bFound As Boolean
    Dim Letter1$

' If gbShowKeyAccidentals=false then,
' in the notes that are in the upper or lower panel,
' hide the # or b accidentals that are
' in the key signature, for ABCPlus input.
' That is, remove the # or b
' character just to the left of the final digit.
' E.g. change C#4 to C4, xF#2 to xF2.
' Do not remove a leading accidental, # b x, that is,
' an altered note, e.g. #C4.

' If a note with accidental is not in the (first)
' key signature, then leave it alone.  E.g. in D major,
' if a key change is made with K2: G Major, then G#3
' can be input or be created by the program in a chord.
' In these cases, display the accidental because it
' was never in the original key signature.

    'If InStr(pN, "-") Then
    '    K = K
    'End If
    If pN = "Ab4" Then
        K = K
    End If
    fcnHideKeyAccidentals = pN
    If Not gbShowKeyAccidentals Then
        GoSub HideKeyAccidentals
    End If
    Exit Function

HideKeyAccidentals:
    K = InStr(2, pN, "#")
    If K > 0 Then
        GoSub Check ' 30apr08
    Else
        K = InStr(2, pN, "b")
        If K > 0 Then
            GoSub Check ' 30apr08
        End If
    End If
    Return
    
'***********************************************
    
    ' Sub InitKey saves the letter and accidental
    ' in gSong(gSongCurr).KeySigIst()
Check:
    Letter1$ = Mid$(pN, K - 1, 1)
    With gSong(gSongCurr)
        For I = 1 To .KeySigUsedIst ' 24jul08
            If Letter1$ = Left$(.KeySigIst(I), 1) Then
                fcnHideKeyAccidentals = Left$(pN, K - 1) _
                    & Mid$(pN, K + 1)
                Exit For
            End If
        Next I
    End With
    Return

End Function

Private Function fcnbCandChord( _
                    pbRomNum() As Boolean, _
                    pInvNeeded() As Integer, _
                    pbFoundMatch2 As Boolean) _
                    As Boolean
    
    ' called by sub FinalTypeChord
    ' called by sub PrevTypeChord

    ' calls function fcnErrAllow
    ' calls function fcnbMaxSATBVarianceOver
    ' calls function fcnNoteIndex
    ' calls function fcnbAlteredFB
    ' calls function fcnMinorProgression
    ' calls function fcnbParallel
    ' calls function fcnCand2ndInv
    ' calls function fcnbHarmProgOk
    ' calls sub Check_6_3_4
    ' calls sub Check_6_4
    ' calls function fcnErr
    ' calls sub MakeChordFromInputNotes
    ' calls sub ExtractRoot
    ' calls function fcnbSecondDominant
    ' calls function fcnReplace

    ' (Calls sub PrintTest2 (in GoSub Debug1))
    ' calls sub PrintTest (in GoSub Debug2)
    ' (Calls function fcnbFindChord (gosub Debug1) the
    '   only use, commented out.)

    ' TODO - review these:
    ' Globals used: gNoteX, gNote()...,
    '       gChord()...
    ' Globals set: gCandLast =,
    '       gNote(gNoteX).FirstCand =,
    '       gNote(gNoteX).LastCand =
    '       gNote(gNoteX).BestCand =
    '       gNote(gNoteX).nGoodCand =
    '       gCand(gCandLast).Index2Ch =
    '       gCand(gCandLast).Error =

    ' Using the current note, gNoteX index,
    ' find all matching chords from array gChord
    ' and store them in array of chord candidates,
    ' gCand.
    ' FirstChord and LastChord either point to the
    ' beginning and end chords in gChord of
    ' the specified scaledeg, or to the first
    ' and last chords of gChord array.

    ' Rule on use of iii6 and vi6.  Each of the
    ' triads usually appears in first inversion
    ' only when the root of the preceding triad
    ' is the same bass note as the first-inversion
    ' note.  There are just two practical
    ' possibilities: I-vi6 and V-iii6. Ottman p 305.

    Dim Chord As Integer ' index to gChord of a
    '   candidate chord for current gNote(gNoteX).
    Dim Ch2 As Integer ' index to gChord of a
    '   candidate chord for prev gNote(gAfterX).
    Dim ChordSave As Integer
    Dim I As Integer
    Dim ILong As Long
    Dim J As Integer
    Dim K As Integer
    Dim C As Integer
    Dim nParallel As Integer
    Dim nNotParallel As Integer
    'Dim Cand2 As Integer
    Dim Cand2 As Long
    Dim Err As String
    Dim Err1 As String ' for fcnCand2ndInv
    Dim Err2 As String ' Final or User
    Dim Err3 As String ' for fcnMinorProgression
    Dim Err4 As String ' for ExtractRoot
    Dim Err5 As String ' for CANDMAX
    Dim Err6 As String ' for FlagChordinError:
    Dim Err7 As String ' for CheckSecondDominant:
    Dim ErrAlt As String ' for fcnMinorProgression and
    '   MatchAltered.
    Dim ErrSave As String
    Dim bBool As Boolean
    Dim nMatch As Integer
    Dim xChordFlag As Byte
    Dim xCandFlag As Byte
    Dim FirstChord As Integer
    Dim LastChord As Integer
    Dim SDeg As Integer
    'Dim NoMatch As Boolean
    Dim E$
    Dim Temp As Integer
    Dim bPrgAny As Boolean
    Dim A$
    Dim Incr As Integer
    Dim Voice As Integer
    Dim Voice2 As Integer

    Dim Bc As Integer
    Dim Chd As Integer
    Dim K1 As Integer
    'Dim SmoothLimit As Integer
    Const LIMIT As Integer = 2 ' halfsteps
    Dim bDupe As Boolean
    Dim bErr As Boolean
    Dim bErrorFourthPassSave As Boolean
    Dim nUserInput As Integer
    Dim nTonesInput As Integer
    Dim bScanNext As Boolean
    ' error flags for fcnMinorProgression:
    Dim bB As Boolean ' basic progression table ok
    Dim bX As Boolean ' extended progression table ok
    Dim b6 As Boolean ' #^6 raised 6th scale deg ok
    Dim b7 As Boolean ' #^7 raised 7th scale deg ok
    Dim b6th7th As Boolean ' check for ascending or
    '   descending 6th or 7th in fcnMinorProgression.
    Dim ERname1 As String
    Dim Note2 As Integer
    Dim NTones As Integer
    Dim SaveIndex2Name(1 To 4) As Integer
    Dim MaxIndex As Integer
    Dim V As Integer
    Dim NCandErrs As Integer
    Dim NCandOk As Integer
    Dim Root$
    Dim ScaleDeg As Integer
    Dim bInKeySig As Boolean
    Dim bAltered As Boolean
    Dim bSecondDominant As Boolean
    Dim VSlash As String
    
'*******************************************
'*******************************************

' Most of these GoSub's also call function
'   fcnErrAllow
' which I omit here for clarity.

Main:
    GoSub Init
    If gNote(gNoteX).bNHTDelayed Then
        'Exit Function
    End If
    GoSub MainLoop
        'GoSub ScanChords
            'GoSub ScanChordsInit
            'GoSub ScanChordsSmooth
                'GoSub MaxSATBVariance
                    'fcnbMaxSATBVarianceOver
            'GoSub ScanSingleChordCriteria
                 'GoSub MatchInputNotes
                 'GoSub MatchAltered
                    'fcnbAlteredFB
                 'GoSub MatchAllowedRomNum
                    'fcnMinorProgression
                 'GoSub MatchSeventh
                 'GoSub MatchFINALTRIPLE
                 'GoSub MatchRange
                 'GoSub MatchBassGap
            'GoSub ScanVoiceLeadingCriteria
                'GoSub CheckParallel
                    'GoSub GetAfterChord
                    'fcnbParallel
                'GoSub CheckInversionNeeded
                    'GoSub CheckInversion
                'GoSub CheckInversion
                    'fcnCand2ndInv
                'GoSub Check7thExcluded
                'GoSub CheckHarmonicProgress
                    'fcnbHarmProgOk
                'GoSub CheckTieAll4
            'GoSub MakeNewCandidate
                'call Check_6_3_4(gCandLast)
                'call Check_6_4(gCandLast)
                'GoSub MakeNewCandFields
                'GoSub SetErrorPrefix
                'fcnErr
        '(END OF GoSub ScanChords)

        'GoSub MakeNewCandidate
    GoSub Test3
    GoSub CheckDupe
        'GoSub GetAfterChord
        'GoSub ScanChords (See above)
    GoSub Conclusion
    Exit Function

'*******************************************
'*******************************************

Init:
    'GoSub Test3
    fcnbCandChord = False
    pbFoundMatch2 = False
    gNote(gNoteX).FirstCand = 0
    gNote(gNoteX).LastCand = 0
    'gNote(gNoteX).BestCand = -1
    gNote(gNoteX).BestCand = 0
    gNote(gNoteX).nGoodCand = 0
    gNote(gNoteX).SolutionNo = 0
    bDupe = False
    GoSub InitPrgAny ' for hard testing
    GoSub InitNUser
    GoSub InitNTones
    GoSub Test3
    Return

'*******************************************

Test3:
    If gSongCurr = 1 Then
        If gNoteX = 65 Then
            I = I
        End If
    End If
    Return

'*******************************************

    ' Use fcnNoteIndex to check for all the tones
    ' input for a note, gNoteX.  There could be four
    ' tones in one note, or a partial chord with the
    ' remaining tones filled from a previous input
    ' note.
    ' If a chord is found matching all four tones that
    ' were input by a user, then use that chord
    ' as the .BestCand, instead of calculating it.
    ' See Conclusion, and Sub ChooseInputChord.
    
    ' Count the number of voices with an input note
InitNUser:
    nUserInput = 0
    For Voice = SOPRANO To BASS
        K = fcnNoteIndex(gNoteX, Voice, bErr) ' call
        If K <> 0 Then
            nUserInput = nUserInput + 1
        End If
    Next Voice
    gNote(gNoteX).ChordInput = 0 ' no special input
    
    ' If all four voices were input for this note
    ' then turn on the bMustAllowError flag so that
    ' it can have any error permitted by
    '   OP: ALLOW error ...
    
    'If nUserInput >= 4 Then
    '    gNote(gNoteX).bMustAllowError = True
    '    gNote(gNoteX).bNHTDelayed = False
    'ElseIf gNote(gNoteX).xNHT And NHTHANG Then
    '    gNote(gNoteX).bNHTDelayed = True
    'End If
    'Return
    
    If nUserInput >= 4 Then
        I = I
        'gNote(gNoteX).bMustAllowError = True
    End If

    If nUserInput >= 4 Then
        GoSub CheckForCrossedVoices ' 7aug08
        'If gNote(gNoteX).bCrossedVoices Then
            ' sort the .Index2Names temporarily
            'GoSub SortCrossedVoices
            ' to be used at ScanChords:
        'End If
    End If
    Return

'*******************************************

    ' see if all four tones were input for this gNoteX,
    ' NOT using tones from an earlier NHF note.
InitNTones:
    nTonesInput = 0
    For Voice = SOPRANO To BASS
        If gNote(gNoteX).Voice(Voice).Index2Name <> 0 Then
            nTonesInput = nTonesInput + 1
        End If
    Next Voice
    If nTonesInput >= 4 Then ' 19oct08
        I = I
        gNote(gNoteX).bMustAllowError = True
    End If
    Return

'*******************************************

    ' OP: ALLOW PRGANY bypasses the Chord
    ' Progression table and allows any chord
    ' to go to any chord (for testing a
    ' difficult piece or debugging).
InitPrgAny:
    bErrorFourthPassSave = gbErrorFourthPass
    gbErrorFourthPass = True
    A$ = fcnErrAllow(gNoteX, "PrgAny") ' call
    If Left$(A$, 1) = "(" Then
        bPrgAny = True
    End If
    gbErrorFourthPass = bErrorFourthPassSave
    Return

'*******************************************

CheckForCrossedVoices:
    gNote(gNoteX).bCrossedVoices = False
    J = 999
    For Voice = SOPRANO To BASS
        'K = gNote(gNoteX).Voice(Voice).Index2Name
        K = fcnNoteIndex(gNoteX, Voice, bErr) ' call
        If J >= K Then
            J = K
        Else
            gNote(gNoteX).bCrossedVoices = True
            Exit For
        End If
    Next Voice
    Return

'*******************************************
Test1:
    'If gNoteX = 1 And SDeg = SUBDOMINANT Then
    'If gNoteX = 7 And SDeg = LEADINGTONE Then
    If gSongCurr = 1 Then
        If gNoteX = 13 Then
            'If SDeg = LEADINGTONE Then
            If SDeg = TONIC Then
                I = I
            End If
        End If
    End If
    Return


'*******************************************

' Get all the possible valid candidates for
' this note gNoteX.
' Stop searching if a chord is found that
' exactly matches the user input chord
' of four tones.
MainLoop:
    ' Only scan the gChord array for those
    ' scale degrees or Roman numerals that
    ' are allowed for this note.
    For SDeg = 1 To TRIADALLOWEDMAX ' 7
        GoSub Test1
        If pbRomNum(SDeg) _
        Or bPrgAny _
        Or gbSmooth Then
            gRomNum = SDeg ' acts as gScaleDeg
            ' See MainChordsForKey
            FirstChord = _
                gKeyChords(gSongKey).FirstChord(gRomNum)
            LastChord = _
                gKeyChords(gSongKey).FinalChord(gRomNum)
            GoSub ScanChords
            If nMatch >= 4 Then
                If nUserInput >= 4 Then
                    Exit For ' found the one 29jul08
                End If
            End If
        End If
    Next SDeg
    If gNoteX = 46 Then
        I = I
    End If
    GoSub CheckCandErrors
    Return

'*******************************************

    ' Make a dummy candidate if no valid one is found.
    ' This allows automatic backtracking.
    
    ' Some chord progressions may have no candidates.

CheckCandErrors:
    If gNote(gNoteX).FirstCand > 0 Then
        GoSub CheckAllCandHaveErrors
    ElseIf nUserInput < 4 Then
        GoSub FlagChordinError ' 19oct08
    Else ' no valid candidate found for gNoteX
        Err = "" ' insurance
        GoSub MakeChord
    End If
    Return

'*******************************************

MakeChord:
        If Not gbNHTInputOption Then
            GoSub FlagChordinError
            Return
        End If
        'If gNoteX = 33 Then
        If gNoteX = 4 Then
            I = I
        End If
        GoSub GetScaleDegreeOfChord
        Call MakeChordFromInputNotes(Err6)
        If Err4 = "" And Err6 = "" Then
            GoSub MakeUpperChordCandidate
        ElseIf Err6 = "nInput" Then ' < 4 input notes
            GoSub FlagChordinError ' 19oct08
        ElseIf Err6 = "Bump" Then ' 30jan09
            'GoSub FlagChordinError ' 19oct08
            GoSub MakeUpperChordCandidate
        ElseIf Err6 = "Unkn4" Then ' 30jan09
            GoSub MakeUpperChordCandidate
        Else ' "unknown" chord not in chord table
            Err7 = ""
            VSlash = ""
            Call SecondDominant(VSlash, Err7)
            If Err7 = "" Then
                bSecondDominant = True
                GoSub MakeUpperChordCandidate
            Else
                GoSub FlagChordinError
            End If
        End If
    Return
    
'*****************************************

MakeUpperChordCandidate:
    gNote(gNoteX).ChordInput = gChordLastUpper
    Chord = gChordLastUpper
    GoSub CheckChordCriteria
    If gNote(gNoteX).FirstCand > 0 Then
        GoSub CheckAllCandHaveErrors
    End If
    If bSecondDominant Then
        GoSub CandSecondDominant
    End If
    
    Return
    
'*****************************************

CandSecondDominant:
    If gNote(gNoteX).LastCand > _
        gNote(gNoteX).FirstCand Then
        I = I
        Stop
    Else
        Cand2 = gNote(gNoteX).FirstCand
        If Cand2 > 0 Then
            gCand(Cand2).VSlash = VSlash
        Else
            I = I
            Stop
        End If
    End If
    Return
    
'*****************************************

' gScaledeg = 8 after on entry to fcnbCandChord
' In Mainloop, gRomNum = SDeg ' acts as gScaleDeg

GetScaleDegreeOfChord:
    Call ExtractRoot(Root$, ScaleDeg, Err4)
    If Err4 <> "" Then
        SDeg = 1
        gScaleDeg = 1
        gRomNum = 1
    Else
        SDeg = ScaleDeg
        gScaleDeg = ScaleDeg
        gRomNum = ScaleDeg
    End If
    ' next 2 lines are from fcnbValidChord 6aug08
    ' with gChordMode replaced by gKeyMode.
    ' override the values set in InitTriads
    gRomNumName = _
        gScale(gKeyMode).RNumName(gScaleDeg)
    gQuality = gScale(gKeyMode).Qual(gScaleDeg)
    Return
    
'*******************************************
    
' For info, build a candidate for this chord
' after sub EditChord, but add "?" after the
' romnum name and the FBname, and
' set .Error="Unkn". 7aug08
FlagChordinError:
    Err = "NoMch"
    Chord = gChordLastUpper
    With gChord(Chord)
        .RomNumName = ""
        .FBName = ""
    End With
    GoSub MakeNewCandidate
    gCand(gCandLast).Error = Err
    If gNote(gNoteX).bCrossedVoices Then
        If gCand(gCandLast).Error = "" Then
            gCand(gCandLast).Error = "(Cross"
        Else ' "X," also means Crossed Voice
            gCand(gCandLast).Error = _
                "X," & gCand(gCandLast).Error
        End If
    End If
    I = I
    Return

'*******************************************

' Since all four tones were input for this gNoteX,
' if each candidate for this gNoteX has an error,
' then add a "(" in front of the error, eg (Bxt,
' so that a .BestCand can be selected from them.
' Must also bump .nGoodCand to get a .BestCand.
CheckAllCandHaveErrors:
    NCandErrs = 0
    NCandOk = 0
    For ILong = gNote(gNoteX).FirstCand To _
        gNote(gNoteX).LastCand
        If gCand(ILong).Error = "" Then
            NCandOk = NCandOk + 1
        Else
            NCandErrs = NCandErrs + 1
            'GoSub Debug7
        End If
    Next ILong
    If NCandOk = 0 _
    And nUserInput >= 4 _
    And gbNHTInputOption = True Then ' 30oct08
        For ILong = gNote(gNoteX).FirstCand To _
            gNote(gNoteX).LastCand
            If gNote(gNoteX).bCrossedVoices Then
                If gCand(ILong).Error = "" Then
                    gCand(ILong).Error = "(Cross"
                Else ' "X," also means "(Cross"
                    gCand(ILong).Error = "X," & gCand(ILong).Error
                End If
            End If
            If Left$(gCand(ILong).Error, 1) <> "(" Then
                gCand(ILong).Error = "(" & gCand(ILong).Error
                    'bump .nGoodCand to create .BestCand
                    gNote(gNoteX).nGoodCand = _
                        gNote(gNoteX).nGoodCand + 1
                    'GoSub Debug7
            End If
        Next ILong
    End If
    Return
    
'*******************************************

Debug7:
    If gCand(I).Error <> "NoMch" _
    And gCand(I).Error <> "Cross" _
    And Left$(gCand(I).Error, 1) <> "(" Then
        Call DebugNote(gNoteX)
        Debug.Print "fcnbcandchord:D7 NCandErrs, Error="; _
            NCandErrs; gCand(I).Error
    End If
    Return
    
'*******************************************

'15jul08
'It seems there is no need to consider
'"Dupe" as an error. Per Adrian Quince, any chord should
'be able to go to itself, since there is no movement of
'the root. In practice, I have to allow all such in the
' program table, and if absent, then use of the "PgmTbl"
'code, will admit a dupe chord as with allowed "(PgmTbl"
'or "(PgmAny" flag.
'    If I disallow Dupe with
'    OP ERROR DUPE
'then it is controlled by
'    OP ALLOW PGMANY.
'This would not affect I to I, since that is always
'allowed, but V to V, etc.
'If dupe is allowed by
'    OP ALLOW DUPE
'then bDupe=True in fcn fcnbCandChord and all checks are
'bypassed.  However, sub CalcVariance (?) will give the
'gNoteX note a higher variance so that other candidates
'are more likely to be selected.

CheckDupe:
    ' As an "DUPE" exception, allow the gNoteX note
    ' to be the duplicate of the gAfterX note.
    If fcnErrAllow(gNoteX, "Dupe") = "(Dupe" Then
        bDupe = True   ' 18mar08 backout 15jul08
        ' TODO
        If gAfterX = 19 Then
            I = I ' testing
        End If
        GoSub GetAfterChord
        If Err2 <> "Final" Then
            FirstChord = Ch2
            LastChord = Ch2
            GoSub ScanChords
        End If
    End If
    Return

'*******************************************

Conclusion:
    'GoSub Debug3
    If gNote(gNoteX).nGoodCand > 0 Then
        fcnbCandChord = True
    End If
    If nUserInput >= 4 Then
        If gNote(gNoteX).ChordInput = 0 Then
            gNote(gNoteX).ChordInput = -1
        End If
    End If
    'GoSub Debug2
    'Exit Function
    Return

'*******************************************

'Debug3:
    'If gProgCol = 11 _
    'And SDeg = 5 Then
    'And pInvNeeded(SDeg) = ROOTPOS Then
    '    I = I ' testing
    'End If
    ' TODO CLEANUP
    'If SDeg = FIFTH Then
    '    Debug.Print "gRomNum="; gRomNum; _
    '        "FirstChord="; FirstChord; _
    '        "LastChord="; LastChord
    '    SDeg = SDeg
    'End If
    'Return

'*******************************************
'*******************************************
' NOT USED 10aug08 Keep as a SORT example?

' Notice that sub ScanTriads
' does not create a chord for a note with crossed
' voices, e.g. if the bass voice has a pitch higher
' than the tenor voice in this note.
SortCrossedVoices:
    For Voice = SOPRANO To BASS
        'SaveIndex2Name(Voice) = _
        '    gNote(gNoteX).Voice(Voice).Index2Name
        GoSub GetfcnNoteIndex
        SaveIndex2Name(Voice) = K
    Next Voice
    'GoSub Debug6
    
    ' Easy Sort:
    ' (1) find the biggest index on each of
    ' four passes of SaveIndex2Name(Voice);
    ' (2) copy it to MaxIndex and
    '     gNote(gNotex).Voice(Voice).Index2Name
    ' (3) make SaveIndex2Name(Voice) negative
    ' to prevent its selection again;
    ' (4) loop;
    For V = SOPRANO To BASS ' 1 To 4
        MaxIndex = 0
        ' find the largest of the remaining voices
        For Voice = SOPRANO To BASS
            If SaveIndex2Name(Voice) > MaxIndex Then
                MaxIndex = SaveIndex2Name(Voice)
                K = Voice ' save the voice of MaxIndex
            End If
        Next Voice
        gNote(gNoteX).Voice(V).Index2Name = _
            MaxIndex
        SaveIndex2Name(K) = -SaveIndex2Name(K) ' negative
    Next V
    'GoSub Debug6
    ' When you copy SaveIndex2Name(Voice) back,
    ' reverse the negative indexes.
    For Voice = SOPRANO To BASS
        SaveIndex2Name(Voice) = Abs(SaveIndex2Name(Voice))
    Next Voice
    Return
    
'*******************************************

GetfcnNoteIndex:
    K = fcnNoteIndex(gNoteX, Voice, bErr) ' call
    K = Abs(K) ' minus means user override
    If K = 0 Then
        Stop
        I = I 'insurance
    End If
    Return
    
'*******************************************

Debug6:
    Debug.Print "fcnbCandChord:D6 gSongCurr,gNoteX,SDeg="; _
        gSongCurr; gNoteX; SDeg; _
        " .Index2Name=";
    For V = SOPRANO To BASS ' 1 To 4
        Debug.Print V; _
            gNote(gNoteX).Voice(V).Index2Name;
    Next V
    Debug.Print
    Return
    'END OF NOT USED 10aug08
    
'*******************************************
' BEGIN GoSub ScanChords:
'*******************************************

' TODO change the index for ScanChords from Chord to Sc

' Full scan for normal candidates for this gNotex.
' If The four tones of gNoteX match those of one
' of the scanned chords, check the other criteria.

' If no scanned chords match gNoteX, then use the
' four tones from note gNoteX, filled in from any
' previous NHT tones, to create a new chord in the
' Upper Chord Table in gChords().  Then check the
' other criteria. 4aug08

ScanChords:
    For Chord = FirstChord To LastChord ' index to gChord
        GoSub Test2
        Err = "" ' 14aug08
        GoSub MatchInputNotes
        'If nMatch >= 4 Then
        If Err <> "NoMch" Then ' 14aug08
            GoSub CheckChordCriteria
        End If
    Next Chord
    Return
        
'******************************************

Test2:
    'If Chord = 647 And gNoteX = 35 Then
    '    I = I
    'End If
    If gSongCurr = 1 Then
        If gNoteX = 54 Then
            If Chord = 83 Then
                I = I
            End If
        End If
    End If
    Return

'*******************************************

' Match all the input tones (.Index2Name > 0)
' of the input gNoteX chord versus the table
' of all valid calculated chords.  This is
' done by index (.Index2Name > 0), not by notename.

' E.g. if gNoteX has two input tones which
' matches one or more chords in the chord table,
' then consider those chords as candidates,
' and go on to check the other criteria.

' Remember gNoteName(.Index2Name)="notename" e.g.G3.

' If any non-zero input element fails to match the chord
' being checked, make a new candidate only for
' the first such "NoMch" case per scaledeg.
' This allows automatic backtracking.

' If all 4 tones/voices were input for gNoteX,
' (a) if it matches a chord, turn on flag
'        gNote(gNoteX).bMustAllowError = True
' so that it can have any permitted error.
' and set:
'        gNote(gNoteX).ChordInput = Chord

' (b) if does not match any chord in the chord
' table, then make a candidate for it anyway
' in the Upper Chord Table as "NoMch".

' DO NOT USE GMAINVOICE TILL THE INFINITE LOOP IS
' DEBUGGED!
' (c) given OP: MISC MAINVOICE soprano,
' if the chord being examined matches the
' soprano tone, then  accept it as a candidate.
' This shows the choices that the composer faced.

' See the gosub MySolution in
' Sub txtMyCand_KeyPress().
' If the user overrode the chord selection
' for a particular note with his own candidate
' by keyboard entry, then the indexes to the
' notenames are stored as negative values for
' every voice that did not have an input card
' entry for that note.

MatchInputNotes:
    'GoSub Test4B
    nMatch = 0
    For Voice = SOPRANO To BASS
        GoSub Test4C
        If gbNHTInputOption Then
            K = fcnNoteIndex(gNoteX, Voice, bErr) ' call
        Else
            K = gNote(gNoteX).Voice(Voice).Index2Name
        End If
    
        K = Abs(K) ' minus means user override
        If K > 0 Then
            If K = gChord(Chord).Voice(Voice).Index2Na _
            Then
                nMatch = nMatch + 1
                If nMatch = nUserInput Then
                    I = I
                End If
            Else ' this chord does not match gNoteX
                If nUserInput < 4 Then
                    Err = "NoMch"
                    Exit For
                ElseIf gMainVoice = 0 Then ' else overflow
                    Err = "NoMch"
                    Exit For
                ElseIf Voice = gMainVoice Then
                    Err = "NoMch"
                    Exit For
                End If
            End If
        End If
    Next Voice
    'GoSub Test4
    If nMatch >= 4 Then
        If gNoteX = 7 Then
           I = I
        End If
        gNote(gNoteX).ChordInput = Chord
    End If
    Return

'*******************************************

Test4C:
    If gNoteX = 13 Then
        If Voice = 4 Then
            If Chord = 325 Then ' see sub FindChord
                GoSub Test4B
                I = I
            End If
        End If
    End If
    Return
'*******************************************

Test4:
    If gNoteX = 2 Then
        If nMatch > 0 And Err = "" Then
            Debug.Print "fcnbCandChord:D8 gnotex="; gNoteX;
            Call DebugChord(Chord)
            I = I
        End If
    End If
    'If gNoteX = 57 And Chord = 1935 Then
    '    GoSub Debug9
    '    Call DebugNote(gNoteX)
    '    Call DebugChord(Chord)
    '    I = I ' testing
    'End If
    Return

'*******************************************

Test4B:
    If gNoteX = 13 Then
        For Voice2 = SOPRANO To BASS
            K = fcnNoteIndex(gNoteX, Voice2, bErr)
            Debug.Print "fcnbCAndChord:D4 Voice, K=fNI, "; _
                "gNoteName = "; Voice2; K; gNoteName(K)
        Next Voice2
        I = I
    End If
    Return

'*******************************************

CheckChordCriteria:
    If SDeg < 1 Or SDeg > 7 Then
        SDeg = 1
    End If
    GoSub ScanChordsInit
    bScanNext = False
    GoSub ScanChordsSmooth
    If bScanNext Then
        GoTo NextChord
    End If

    GoSub ScanSingleChordCriteria
    If bScanNext Then
        GoTo NextChord
    End If
    GoSub ScanVoiceLeadingCriteria

    ' Enter a new candidate in the cand table,
    ' with or without an Err code:
'EnterNewCand:
    GoSub MakeNewCandidate

' Come here to NextChord if a basic match fails
' otherwise save/display new cand with any Err found
NextChord:
    'Next Chord
    Return

'*******************************************

ScanChordsInit:
     Err = ""
     If bDupe Then
         Err = fcnErrAllow(gNoteX, "Dupe") ' call
         I = I ' testing
     End If
     ' TODO cleanup
    'gbNewCand = True ' reset for fcnErrAllow
    'If gbNewCand Then ' reset in fcnbCandChord
    '    gbNewCand = False
         gLowestErrNum = 99
         gNumCandErrs = 0
         gSingleAllowedErrNum = 0
         gLowestAllowedErrNum = 99
         gNumAllowedCandErrs = 0
         gxCandErrorBits = 0 ' reset all error bits
    'End If
    Return

'*******************************************

ScanChordsSmooth:
    gbSmooth = False
    If gAfterX > 0 Then
        If gbSmoothOption Then
            GoSub MaxSATBVariance
        End If
        If Not gbSmooth Then
            If bDupe Then
                '
            ElseIf bPrgAny Then
                '
            ElseIf SDeg >= 1 And SDeg <= 7 Then
                If pbRomNum(SDeg) Then  ' 18mar08
                    '
                End If
            Else
                'GoTo NextChord
                bScanNext = True
            End If
        End If
    End If
    Return

'*******************************************

     ' This section checks single chord criteria:
ScanSingleChordCriteria:
    ' TODO
    'If gAfterX = 18 And Chord = 206 Then
    '    J = J
    'End If
    'GoSub MatchInputNotes 4aug08
    ' Skip all the "NoMch" errors
    ' per each scale degree (except for testing)
    'If Err = "NoMch" Then
        'If gNote(gNoteX).ChordInput = -1 Then
            ' 4 tones were input for this gNoteX
            ' but no matching chord was found;
            ' make a candidate for it anyway.
        'Else
    '        bScanNext = True ' GoTo NextChord
    '        GoTo SingleExit
        'End If
    'End If

    ' From now on, make a candidate for each chord
    ' whether the Err code is empty or not.
    GoSub MatchAltered
    GoSub MatchAllowedRomNum
    GoSub MatchSeventh
    GoSub Check7thExcluded
    
    GoSub MatchFINALTRIPLE
    GoSub MatchRange
    GoSub MatchBassGap
SingleExit:
    Return

'*******************************************

     ' This section checks voice leading criteria:
ScanVoiceLeadingCriteria:
     If gAfterX > 0 Then
        If gNote(gAfterX).BestCand <= 0 Then
            GoTo ScanReturn1
        End If
         GoSub CheckParallel
        'If Err <> "" Then GoTo EnterNewCand
         If Err2 = "Final" Then
             GoSub CheckInversion
         Else
             GoSub CheckInversionNeeded
         End If
        'If Err <> "" Then GoTo EnterNewCand
         'GoSub Check2ndInv
        'Err1 = fcnCand2ndInv(Chord)
        'If Err <> "" Then GoTo EnterNewCand
         GoSub Check7thExcluded
        'If Err <> "" Then GoTo EnterNewCand
         GoSub CheckHarmonicProgress
         GoSub CheckTieAll4 ' must come after
         '  GoSub CheckParallel
     Else
         GoSub CheckInversion
     End If
    'If Err <> "" Then GoTo EnterNewCand
    'pbFoundMatch2 = True
ScanReturn1:
     Return

'*******************************************
' END GoSub ScanChords:
'*******************************************

' Turn on the OP: MISC SMOOTH ON option
' if no voice moves more than a whole
' step between gNoteX and gAfterX, but
' not if the chords are identical.
MaxSATBVariance:
   'If fcnbMaxSATBVarianceOver(Chord, LIMIT) Then
   '    If Not (pbRomNum(SDeg) Or bPrgAny) Then
   '        GoTo NextChord
   '    End If
   'Else
   '    gbSmooth = True
   'End If
   'Return

    If Not fcnbMaxSATBVarianceOver(Chord, LIMIT) _
    Then ' call
        gbSmooth = True
    End If
    Return

'*******************************************

Debug9:
     Debug.Print "fcnbCandChord="; Voice; K; Chord; _
     gNoteName(K); _
     gChord(Chord).Voice(Voice).Index2Na;
     For I = SOPRANO To BASS
          Debug.Print gNoteName(gChord(Chord).Voice(I).Index2Na);
     Next I
     Debug.Print
     Return
     
'*******************************************

' If a note is to be altered by a figured bass
' entry, e.g. "b6" or ""#3", check to see if there
' is a note at that interval above the bass.
' If not, err out this candidate.

' TODO a doubled note should not be altered.
' e.g. if the tonic is doubled, then it should
' not be raised by #^1 or #3.

' gChord(Chord).AlteredChord = 6 means #^6, or
' raised sixth scale degree in Minor.
' gChord(Chord).AlteredChord = 7 means #^7.

MatchAltered:
        ' TODO 11/19/6
        'If Chord >= 636 And Chord <= 636 _
        'And gNoteX = 21 Then
        'If Chord >= 581 And Chord <= 581 _
        'And gNoteX = 7 Then
        If Chord >= 76 And Chord <= 76 _
        And gNoteX = 25 Then
            I = I
        End If
    ErrAlt = ""
    If gChord(Chord).AlteredChord = 0 Then
        If gNote(gNoteX).bAlterAboveBass _
        Or gNote(gNoteX).bAlterSDeg Then
            ' gnotex is altered but chord is not
            Err = fcnErrAllow(gNoteX, "Above")
            Return
        Else ' neither chord nor gnotex is altered
            Return
        End If
    ElseIf gChord(Chord).AlteredChord = 3 _
    Or gChord(Chord).AlteredChord = 6 _
    Or gChord(Chord).AlteredChord = 7 Then
    ' chord has altered #^6 or #^7
        If gNote(gNoteX).bAlterAboveBass _
        Or gNote(gNoteX).bAlterSDeg Then
            ' both chord and gnotex are altered
            If fcnbAlteredFB(gNoteX, Chord) Then ' call
                Return
            Else ' chord failed the increm or double test
                Err = fcnErrAllow(gNoteX, "Increm")
                Return
            End If
        Else ' chord is altered but gnotex is not
            If gChord(Chord).AlteredChord = 3 Then
                Err = fcnErrAllow(gNoteX, "Above")
                Return
            Else
                ErrAlt = "Above" ' see MatchAllowedRomNum
                Return
            End If
        End If
    Else
        Stop
    End If
    Return

'*******************************************

MatchAllowedRomNum:
    ' Tonic I or i triad can progress to any triad
    If gChord(Chord).ScaleDeg = TONIC Then ' 1
        Return
    End If

    ' gbSmooth is true if all four voices in one
    ' chord move very smoothly to the next
    ' chord, in not more than a whole step,
    ' but not if the chords are identical.
    ' If gbSmooth, then accept any harmonic
    ' progression. See Ottman p 177 Bach e.g.
    If gbSmooth Then
        Return
    End If

    'Temp = gChord(Chord).ScaleDeg
    'If Temp = 2 _
    'And not bDupe _
    'And gRomNum = 2 Then
        'Debug.Print "g,t="; gRomNum; Temp
        'Chord = Chord ' testing
    'End If
    Select Case gKeyMode
    'Case MINORMELUP
    Case MINORMELUP, MINORNAT, MINORHARM
        ' gKeyMode=MINORMELUP with #^6, #^7's
        If Not fcnMinorProgression( _
            Chord, bB, bX, b6, b7, b6th7th) _
        Then ' call
            If Not bB Then
                Err3 = fcnErrAllow(gNoteX, "PrgBas")
                If Left$(Err3, 1) = "(" Then
                    Err = Err3
                    If Not bX Then
                        Err = fcnErrAllow(gNoteX, "PrgExt")
                    End If
                End If
            End If
            
            If b6th7th Then
                If Not b6 Then
                    Err = fcnErrAllow(gNoteX, "Prg6th")
                End If
                If Not b7 Then
                    Err = fcnErrAllow(gNoteX, "Prg7th")
                End If
            Else
                If ErrAlt = "Above" Then
                    Err = fcnErrAllow(gNoteX, "Above")
                End If
            End If
        End If
    Case Else
    'If gKeyMode = MAJOR _
    'Or gKeyMode = MINORNAT _
    'Or gKeyMode = MINORHARM Then
        If bDupe Then
            '
        ElseIf bPrgAny Then
            If Not pbRomNum(SDeg) Then
                If SDeg = gChord(Chord).ScaleDeg Then
                    'If gChord(Chord).RomNumName <> "" Then
                    If Err4 = "" And Err6 = "" Then
                        ' skip if "Unkn" candidate 7aug08
                        If gNoteX = 113 Then
                            I = I
                        End If
                        Err = fcnErrAllow(gNoteX, "PrgAny")
                    End If
                End If
            End If
        Else
            If gRomNum <> gChord(Chord).ScaleDeg Then
                'If gChord(Chord).RomNumName <> "" Then
                If Err4 = "" And Err6 = "" Then
                    If gNoteX = 113 Then
                        I = I
                    End If
                    Err = fcnErrAllow(gNoteX, "PrgAny")
                End If
            End If
        End If
    End Select
    Return

'*******************************************

MatchSeventh:
    bBool = gNote(gNoteX).bSeventh
    If bBool Then
        If bBool <> gChord(Chord).bSeventh Then
            Err = fcnErrAllow(gNoteX, "Not7th")
            'GoTo NextChord
        End If
    End If
    Return

'*******************************************

MatchFINALTRIPLE:
    ' If the chord is a tonic triple root
    ' with the root in the bass,
    ' then the note must be final also. Ottman.
    ' Bach example 00360b allows tonic triple as
    ' first chord.
    ' Allow a triple root in any inversion
    ' with the error (Triple.  Per Bach examples?
    If gChord(Chord).bTonicTriple Then
        'If Not gNote(gNoteX).bFinalChord _
        'And Not gNote(gNoteX).bFirstChord Then
        If Not gNote(gNoteX).bFinalChord Then ' 29nov08
            If gNoteX = 54 Then
                I = I
            End If
            Err = fcnErrAllow(gNoteX, "NotFin")
            'GoTo NextChord
        End If
    ElseIf gChord(Chord).bTripleRoot Then
        If gNoteX = 54 Then
            I = I
        End If
        Err = fcnErrAllow(gNoteX, "Triple")
        'GoTo NextChord
    End If
    Return

'*******************************************

' Each voice in the chord tested must fall in
' its Best Range (less than a full range), e.g.
' OP:    RANGE   TENOR   BEST    C2-E3
' There is no need to check that the tones of a
' chord lie within range, because sub ScanTriads
' does that.
MatchRange:
    With gSong(gSongCurr)
        For Voice = SOPRANO To BASS
            K = gChord(Chord).Voice(Voice).Index2Na
            If K > .RangeBestTop(Voice) _
            Or K < .RangeBestBot(Voice) Then
                Err = fcnErrAllow(gNoteX, "BRANGE")
                'If Err = "(BRANGE" Then
                '    Voice = Voice ' testing
                'End If
                Exit For
            End If
        Next Voice
    End With
    Return

'*******************************************

' This OPTION
' OP: MISC BassGap 12
' means that the Tenor cannot be more than
' 12 half steps above the Bass.
' This allows the pianist to have no more than
' an octave to play in the Left$ hand.
MatchBassGap:
    With gSong(gSongCurr)
        J = gChord(Chord).Voice(TENOR).Index2Na
        K = gChord(Chord).Voice(BASS).Index2Na
        If J > K + .BassGap Then
            Err = fcnErrAllow(gNoteX, "BassGap")
            'If Err = "(BASSGAP" Then
            '    J = J ' testing
            'End If
        End If
    End With
    Return

'*******************************************

CheckParallel:
    'GoSub Debug5
    If gNoteX = 12 Or gNoteX = 13 Then
       I = I
    End If
    If gNote(gNoteX).bFinalChord Then
        Return
    End If
    nParallel = 0
    nNotParallel = 0
    Ch2 = 0
    'GoSub GetAfterChord
    GoSub GetChordofNextNote ' 27jul08
        ' TODO change fcnbParallel return
        ' value to Err if no need for
        ' the nParallel count, e.g.
        'Err= fcnbParallel( _
        '    Chord, Ch2) Then ' call
    If Err2 = "" And Ch2 > 0 Then
        If fcnbParallel( _
            Chord, Ch2, Err) Then ' call
            nParallel = nParallel + 1
        Else
            nNotParallel = nNotParallel + 1
        End If
    End If
    Return

'*******************************************

' Check for parallel fifths and octaves P5 and P8,
' and checks for crossing tones, e.g. BxT,
' should be between this gNoteX candidate and
' the next (non-bar) note.  If the next note does not
' have a full (valid or invalid) chord of 4 tones
' then create a dummy chord at gChord(CHORDSMAX + 1).

' gChord(CHORDSMAX + 1) is only used for a dummy chord
' for fcnbCandChord to pass ch2 to fcnbParallel
' in case the note after gNoteX does not have 4 tones.

GetChordofNextNote:
    ' find first note after gNoteX that is not a
    ' bar or 4-rest.
    Note2 = fcnNoteAfter(gNoteX)
    If Note2 = 0 Then
        Err2 = "Final"
        GoTo GetExit1
    ElseIf gNote(Note2).BestCand > 0 Then
        GoSub GetAfterChord
    End If
GetExit1:
    Return
    
' NOT USED 1dec08
    'ElseIf Note2 = gAfterX Then
    '    GoSub GetAfterChord
    '    GoTo GetExit1
    '    GoTo GetExit1
    'Else
    '    GoSub MakeDummyChordCh2
    'GoSub CountTonesinNote2
        
'*******************************************

' Make a dummy chord at Ch2 = CHORDSMAX+1
' and fill in the indexes into gNoteName
' from the non-zero tones in Note2 and then
' from the (non-zero) tones in this
' candidate, Chord, for gNotex:
'   gChord(Ch2).Voice(v).Index2Na
' Then the indexes into gNoteName from
' Chord and Ch2 can be compared in fcnbParallel.
MakeDummyChordCh2:
    Ch2 = CHORDSMAX + 1
    For Voice = SOPRANO To BASS
        If gNote(Note2).Voice(Voice).Index2Name = 0 Then
            gChord(Ch2).Voice(Voice).Index2Na = _
                gChord(Chord).Voice(Voice).Index2Na
        Else
            gChord(Ch2).Voice(Voice).Index2Na = _
                gNote(Note2).Voice(Voice).Index2Name
        End If
    Next Voice
    'GoSub Debug5
    Return

'*******************************************

Debug5:
    If gSongCurr = 1 _
    And gNoteX = 5 Then
        Debug.Print "fcnbCandChord:D5 gNoteX Note2 gAfterX="; _
            gNoteX; Note2; gAfterX
        Call DebugChord(Chord)
        Call DebugChord(Ch2)
        I = I
    End If
    Return
    
'*******************************************

CountTonesinNote2:
    NTones = 0
    For Voice = SOPRANO To BASS
        If gNote(Note2).Voice(Voice) _
        .Index2Name > 0 Then
            NTones = NTones + 1
        End If
    Next Voice
    Return
    
'*******************************************

GetAfterChord:
    'Cand2 = gNote(gAfterX).BestCand ' best
    Cand2 = gNote(Note2).BestCand ' best
    ' candidate for the gNote(gAfterX) note
    ' that follows this gNote(gNoteX) note.
    If Cand2 <= 0 Then
        ' If the note following this one
        ' has no valid candidate, flag this
        ' one with the eye-catcher "Final"
        ' in Err2
        ' so as to keep on processing it.
        'Ch2 = 0 ' insurance
        Err2 = "Final"
        'Err2 = "NextChord"
    Else
        Ch2 = gCand(Cand2).Index2Ch ' gChord index
        Err2 = ""
    End If
    Return

'*******************************************

CheckInversionNeeded:
    ' gInversionNeeded is set for these cases:
    ' The first chord must be in ROOTPOS in:
    ' iii-iio6, V-iii6, V-IV6, I-vi6
    ' (I allow V6-IV6 since Bach does in Chorale 1.)
    ' I assume the first chord must be in ROOTPOS in:
    ' vi-iii-IV or VI-III-iv which can be replaced by:
    ' vi-iii-iio6 or VI-III-ii6.

    ' WARNING -DO NOT COME TO THIS GOSUB IF
    ' called by sub FinalTypeChord
    ' because of the Dummy() parm.
    'If gProgCol = 11 _
    'And SDeg = 5 Then
    '  'And pInvNeeded(SDeg) = ROOTPOS Then
    '    SDeg = SDeg
    'End If

    ' TODO check inversion for bDupe
    If bDupe Then
        Return
    End If

    gInversionNeeded = pInvNeeded(SDeg)
    If gInversionNeeded = NOINVERSIONNEEDED _
    Then
        GoSub CheckInversion
    Else
        If gInversionNeeded <> _
        gChord(Chord).Inversion Then
            'If gAfterX = 3 Then
            '    SDeg = SDeg ' testing
            'End If
            Err = fcnErrAllow(gNoteX, "ProgNv")
        Else
            GoSub CheckFBOption
        End If
    End If
    Return

'*******************************************

    ' If no inversion request was input for a note,
    ' any inversion is ok, so do the special
    ' second inversion checks.
    ' If an inversion request was input for a note,
    ' but this chord's inversion doesn't match it,
    ' then reject the chord as a candidate.
    ' The same goes for the .bSeventh quality.
    
    ' Any inversion input on an FB: card overrides
    ' the OP: FIGBASS option values.

CheckInversion:
    If gSongCurr = 1 And gNoteX = 594 Then
        I = I
    End If

    If gNote(gNoteX).Inversion = _
    NOINVERSIONNEEDED Then
        'GoSub CheckFBOption
        Err1 = fcnCand2ndInv(Chord) ' check 2nd inv
        If Err1 <> "" Then
            Err = Err1
        End If
        ' TODO 11/25/6
        GoSub CheckFBOption
    Else
        If gNote(gNoteX).Inversion = _
        gChord(Chord).Inversion _
        And gNote(gNoteX).bSeventh = _
        gChord(Chord).bSeventh Then
            Err1 = fcnCand2ndInv(Chord) ' check 2ndInv
            If Err1 <> "" Then
                Err = Err1
            End If
            'GoSub CheckFBOption 7may08
        Else
            Err = fcnErrAllow(gNoteX, "Invr")
        End If
    End If
    Return

'*******************************************

    ' The FigBass array is copied from sub
    ' StoreOptionFigBass.
    ' The Inversion array beneath it shows
    ' the corresponding Inversion numbers,
    ' where the upper four are that of
    ' 7th chords.
    'FigBass = Array("", _
    '"53", "6", "64", "7", "65", "43", "42")
    'Inversion = Array("", _
    '" 0", "1", "2", "0", "1", "2", "3")
CheckFBOption:
    If gChord(Chord).bSeventh Then
        I = gChord(Chord).Inversion + 3
    Else
        I = gChord(Chord).Inversion
        If I = 1 Then
            I = I ' FB=6
        End If
    End If
    'If Not gbFigBassAllowed(I) Then ' 8may08
    If Not gSong(gSongCurr).bFigBassAllowed(I) Then
        If gNoteX = 7 Then
            I = I
        End If
        Err = fcnErrAllow(gNoteX, "FBopt")
    End If
    Return
    
'*******************************************

Check7thExcluded:
    If gNote(gNoteX).bSeventhExcluded Then
        If gChord(Chord).bSeventh Then
            Err = fcnErrAllow(gNoteX, "X7th") ' see if the
            ' 7th of the chord is allowed
            ' despite OP: EXCLUDE CHORD7TH
            ' to temporarily permit a solution.
        End If
    End If
    Return

'*******************************************

CheckHarmonicProgress:
    ' gChord(Chord).xHarmProgFlag is set only
    ' in Sub EditChord for a Mediant or
    ' Submediant chord.
    xChordFlag = gChord(Chord).xHarmProgFlag
    ' TODO CLEANUP
    'If gChord(Chord).ScaleDeg = SUBMEDIANT _
    'And gChord(Chord).Inversion = ROOTPOS Then
    '    Chord = Chord ' dummy Stop
    'End If
    xCandFlag = 0
    If xChordFlag <> ALLZERO Then
        If Not fcnbHarmProgOk( _
            xChordFlag, xCandFlag) Then ' call
            Err = fcnErrAllow(gNoteX, "Prog63")
            'GoTo NextChord
        End If
    End If
    Return

'*******************************************

    'If you flag a note (V1:) with "-*", e.g. F#4-*,
    ' in order to propagate the TIE hyphen's to all
    ' voices, then force this note to have the same
    ' chord index as the previous one.  In ABC, the
    ' TIE hyphen following a note name only causes
    ' a tie mark if both have the same pitch.
    ' You can bypass the check, if needed, with
    '   OP: MISC    ALLOW   Tie4.

CheckTieAll4:
    For Voice = SOPRANO To BASS
        If gNote(gNoteX).Voice(Voice).xTie And TIEALL4 Then
            If Chord = Ch2 Then
                Err = ""
            Else
                Err = fcnErrAllow(gNoteX, "Tie4")
            End If
            Exit For
        End If
    Next Voice
    Return

'*******************************************

MakeNewCandidate:
    If gCandLast >= CANDMAX Then
        GoTo Err5
    Else
        If bDupe Then
            Err = fcnErrAllow(gNoteX, "Dupe")
        End If
        GoSub MakeNewCandFields
        'GoSub Debug4
        GoSub SetErrorPrefix

        ' Update .FirstCand, .LastCand., nGoodCand
        'gCand(gCandLast).Error = Err
        If gNote(gNoteX).FirstCand = 0 Then
            gNote(gNoteX).FirstCand = _
                gCandLast
        End If
        gNote(gNoteX).LastCand = gCandLast
        If fcnErr(Err) = "" Then ' call
            gNote(gNoteX).nGoodCand = _
                gNote(gNoteX).nGoodCand + 1
        End If
    End If
    Return

'*******************************************

MakeNewCandFields:
    gCandLast = gCandLast + 1
    ' re-initialize with gCand(0) for cmdNextFile
    If gbFileNameNext Then
        If gCand(gCandLast).bUsed Then
            gCand(gCandLast) = gCand(0)
        End If
    End If
    gCand(gCandLast).bUsed = True
    
    gCand(gCandLast).Index2Ch = Chord ' save
    ' index of this candidate chord
    If gNote(gNoteX).InputCand = 0 Then
        If gNote(gNoteX).ChordInput > 0 Then
            gNote(gNoteX).InputCand = gCandLast
        End If
    End If
    ' TODO next line probably not needed:
    gCand(gCandLast).Variance = 0!
    gCand(gCandLast).bSmooth = gbSmooth
    gbSmooth = False
    gCand(gCandLast).bDupe = False
    gCand(gCandLast).OuterVariance = 0!
    gCand(gCandLast).InnerVariance = 0!
    gCand(gCandLast).xOuterMotion = 0
    gCand(gCandLast).xInnerMotion = 0
    gCand(gCandLast).OuterGraphic = ""
    gCand(gCandLast).InnerGraphic = ""

    gCand(gCandLast).xHarmProgFlag = _
        xCandFlag
    ' Set more bits in .xHarmProgFlag
    Call Check_6_3_4(gCandLast)
    Call Check_6_4(gCandLast)
    Return

'*******************************************

'Debug4:
    ' TODO CLEANUP
    'If xCandFlag > 0 Then
        'Debug.Print "CandFlag="; xCandFlag
        'Stop
    'End If
    'If (gNoteX = 32 Or gNoteX = 31) _
    'And ucase$(Err) = "2INV" Then
    '    Chord = Chord ' testing
    'End If
    'Return

'*******************************************

' Add a prefix. if needed, in the Error field
' on the candidates grid,
' If an Error but not a Rest note, then return:
'   pbFoundMatch2 = True

SetErrorPrefix: ' see fcnErrAllow
    pbFoundMatch2 = False
    If Err = "NoMch" Then
        GoTo NoMatch
    ElseIf Err = "(Cross" Then
        GoTo NoMatch
    ElseIf Err = "Cross" Then
        GoTo NoMatch
    End If
    ErrSave = Err
    If gNumAllowedCandErrs > 1 Then
        Err = "(<" & gErrNameLC(gLowestAllowedErrNum)
    ElseIf gNumCandErrs > 0 Then
        Err = gErrNameLC(gLowestErrNum)
    ElseIf gSingleAllowedErrNum > 0 Then
        Err = "(" & gErrNameLC(gSingleAllowedErrNum)
    Else
        If Err <> "Rest" Then
           Err = ""
        End If
        pbFoundMatch2 = True
    End If
    GoSub CheckERName
NoMatch:
    GoSub AdjustOverlappingErrors
    gCand(gCandLast).Error = Err
    'Debug.Print "MC:"; Chord; ":"; Err
Return

'*******************************************

' If you specified an error name, e.g. SIM4, on an
'   ER:     sim4
' card for this gNoteX note, saved in .ERName,
' then do not allow any other good candidates
' for this note.
' If a paren ( or (< is found in column 1 of Err
' as a flag that the error name is allowed, then
' strip off the ( or (< when the error name
' doesn't match gNoteX's ERName.
' Do not need:
'   OP:    ALLOW   SIM4     or
'   OP:    ALLOW   PASS1   SIM4
' to force this note to choose the best one from
' the (SIM4 candidates. 8may08
' The point is, that you do not have to ALLOW SIM4
' for other notes in the song if you do not want to.
CheckERName:
    ERname1 = gNote(gNoteX).ERName
    If ERname1 <> "" Then
        Err1 = UCase$(Err)
        If Err = "" Then
            Err = "ERopt"
        ElseIf ERname1 = Err1 Then
            Err = "(" & Err '
        ElseIf Left$(Err1, 2) = "(<" Then
            If Mid$(Err1, 3) <> _
            ERname1 Then
                Err = Mid$(Err, 3)
            End If
        ElseIf Left$(Err1, 1) = "(" Then
            If Mid$(Err1, 2) <> _
            ERname1 Then
                Err = Mid$(Err, 2)
            End If
        End If
    End If
    Return

'*******************************************

' If the higher voice falls below the lower
' voice, e.g. T2 < B1, Tenor falls below Bass,
' then reverse the letters in the error field
' on the upper grid.  See fcnbParallel.
AdjustOverlappingErrors:
    I = InStr(ErrSave, "=")
    If I > 0 Then
        If InStr(ErrSave, "BxT") Then
            Err = fcnReplace(Err, "BxT", "TxB")
        ElseIf InStr(ErrSave, "TxA") Then
            Err = fcnReplace(Err, "TxA", "AxT")
        ElseIf InStr(ErrSave, "AxS") Then
            Err = fcnReplace(Err, "AxS", "SxA")
        End If
    End If
    Return

'*******************************************

Debug1:
    'And gNoteX = gSong(gSongCurr).FirstNote _
    '+ 218 Then
    If gSongCurr = 1 _
    And gNoteX = 44 Then
        Debug.Print "fcnbCandChord:D1 TM="
        For J = 1 To TRIADALLOWEDMAX
            If pbRomNum(J) Then
                Debug.Print J;
            End If
        Next J
        Debug.Print
        'call PrintTest2(gNoteX)
        Chord = Chord ' testing
    End If
    ' Use "G1" instead of "G 1" in:
    'If fcnbFindChord(Chord, "Bb3", "Eb3", _
    '                "Eb2", "G1") Then ' call
    '    call PrintTest(gNoteX)
    '    Chord = Chord ' testing
    'End If
    Return

'*******************************************

Debug2:
    Return
    If gSongCurr = 34 _
    And gNoteX = gSong(gSongCurr).FirstNote _
    + 6 Then
        Debug.Print "fcnbCandChord:D2"
        Call PrintTest(gNoteX)
        Chord = Chord ' testing
    End If
    Return

'*******************************************

Err5:
    E$ = "fcnbCandChord:E5 reached CANDMAX=" & _
        CANDMAX & " in Song no=" & gSongCurr & _
        " at Note no=" & gNoteX & _
        " FirstCand=" & gNote(gNoteX).FirstCand & _
        " LastCand=" & gNote(gNoteX).LastCand
    txtPrint.Text = E$
    Debug.Print E$
    ' TODO
    Chord = Chord ' testing
    Stop
    I = I ' testing

End Function


Private Sub SecondDominant( _
                    pVSlash As String, _
                    pErr7 As String)
    
    ' called by function fcnbCandChord
    
    ' If an input note gNoteX is altered by an
    ' accidental or natural sign that is not
    ' in the current key signature, that is,
    ' the original key signature or a modulated key,
    ' then the note may be in a Second Dominant
    ' relation to the following gAfterX note.
    
    ' Since gNoteX has an altered tone,
    ' eg C# in G Major, then the chord
    ' is not found in the current Chord Table,
    ' G Major.
    
    ' If gNoteX is found to be a Second Dominant,
    ' then function fcnbCandChord will create
    ' a new chord in the Upper Chord Table,
    ' so that it can create a candidate for
    ' gNoteX in the gCand table.
    
    ' If the following gAfterX note is major or
    ' minor in quality (not augmented or
    ' diminished), then find the tone in
    ' chord position 5, or the 5th of the chord.
    ' Eg if gAfterX is C E G C, then G is its 5th.K
    ' If one or more G's exist as one of theim I as
    ' tones in gNoteX, then treat it as the root.
    ' Compute the offset from the root G to
    ' the other three tones, modulo 12.
    ' If the offsets are 0 or 4 or 7 half-steps,
    ' then gNoteX is a major triad, acting as a
    ' dominant chord to gAfterX, written V/.
    ' Return pVSlash="V/".
    
    ' If the offsets are 0 or 4 or 7 or 10 half-steps,
    ' then gNoteX is a major triad  and
    ' dominant seventh, acting as a
    ' dominant 7th chord to gAfterX, written V7/.
    ' Return pVSlash="V7/".
    
'*****************************************
    
    Dim I As Integer
    Dim K As Integer
    Dim A$
    Dim Cand2 As Long
    Dim Chord2 As Integer
    Dim bErr As Boolean
    Dim Err2 As String
    Dim Voice As Integer
    Dim Name$
    Dim Letter$
    Dim Digit$
    Dim Accid$
    Dim Key$
    Dim RootIndex As Integer ' root index of gNoteX
    Dim RootIndex2 As Integer ' root index of gAfterX
    Dim RootName As String
    Dim RootLetter As String
    Dim RootVoice As Integer
    Dim NoteName(4) As String
    Dim NoteLetter(4) As String
    Dim NoteAccid(4) As String
    Dim NoteIndex(4) As Integer ' Sop to Bas, 1 to 4
    Dim bInKeySig As Boolean
    Dim bAltered As Boolean
    Dim HalfStep(11) As Integer
    
'*****************************************
'*****************************************

Main:
    GoSub Init:
    GoSub MainLine
    Exit Sub
    
'*****************************************
'*****************************************

Init:
    pErr7 = ""
    Return
    
'*****************************************

MainLine:
    GoSub CheckgAfterXQuality
    If pErr7 = "" Then
        GoSub CheckSecondDominant
    End If
    Return
    
'*****************************************

' best candidate for the gNote(gAfterX) note
' that follows this gNote(gNoteX) note.
CheckgAfterXQuality:
    GoSub GetgAfterXChord2
    If Chord2 = 0 Then
        pErr7 = "Final"
    Else
        If gChord(Chord2).Quality <> QUALMAJOR _
        And gChord(Chord2).Quality <> QUALMINOR Then
            pErr7 = "Qual"
        Else
            GoSub FindThe5thofgAfterX
            If RootLetter = "" Then
                pErr7 = "Root"
            Else
                GoSub GetgNoteXNames
                GoSub FindgNoteXRoot
                If RootVoice = 0 Then
                    pErr7 = "Root2"
                Else
                    I = I
                    'stop
                End If
            End If
        End If
    End If
    Return

'*****************************************

GetgAfterXChord2:
    Chord2 = 0
    Cand2 = gNote(gAfterX).BestCand
    If Cand2 <= 0 Then
        ' If the note following this one
        ' has no valid candidate, flag this
        ' one with the eye-catcher "Final"
        ' in Err2 so as to keep processing it.
        Err2 = "Final"
    Else
        Chord2 = gCand(Cand2).Index2Ch ' gChord index
        Err2 = ""
    End If
    Return
    
'*****************************************

' Get the letter name of the fifth of the gAfterX
' chord.
' If gNoteX is to be a Second Dominant
' to gAfterX, then this letter must be the RootName
' of gNoteX.
FindThe5thofgAfterX:
    RootIndex2 = 0
    For Voice = SOPRANO To BASS ' 1 to 4
        If gChord(Chord2).Voice(Voice).ChordPos = 5 Then
            RootIndex2 = _
                gChord(Chord2).Voice(Voice).Index2Na
            RootName = gNoteName(RootIndex2)
            Exit For
        End If
    Next Voice
    ' Get the letter name of  the 5th of gAfterX.
    RootLetter = ""
    If RootName <> "" Then
        Name$ = RootName
        GoSub GetLetter
        RootLetter = Letter$
    End If
    Return
    
'*****************************************

GetLetter:
    Letter$ = ""
    For I = 1 To Len(Name$)
        A$ = Mid$(Name$, I, 1)
        If A$ >= "A" Then
            If A$ <= "G" Then
                Letter$ = A$
                Exit For
            End If
        End If
    Next I
    Return
    
'*****************************************

GetgNoteXNames:
    For Voice = SOPRANO To BASS
        Name$ = ""
        K = gNote(gNoteX).Voice(Voice).Index2Name
        If K > 0 Then
            Name$ = gNote(gNoteX).Voice(Voice).Name
        ElseIf gbNHTInputOption Then
            K = fcnNoteIndex(gNoteX, Voice, bErr) ' call
            ' better to get the actual input name
            If K > 0 Then
                Name$ = gNoteName(K)
            Else
                Stop
            End If
        End If
        NoteIndex(Voice) = K
        NoteName(Voice) = Name$
        GoSub ParseChars ' get Letter$,Accid$ from Name$
        NoteLetter(Voice) = Letter$
        NoteAccid(Voice) = Accid$
    Next Voice
    'GoSub Debug1
    Return
    
'*****************************************

ParseChars:
    Letter$ = ""
    Digit$ = ""
    Accid$ = ""
    For I = 1 To Len(Name$)
        A$ = Mid$(Name$, I, 1)
        If A$ >= "A" Then
            If A$ <= "G" Then
                Letter$ = A$
            End If
        Else
            If A$ = "#" _
            Or A$ = "b" _
            Or A$ = "x" _
            Or A$ = "=" Then
                Accid$ = A$
            End If
        End If
    Next I
    Return
    
'*****************************************

Debug1:
    Debug.Print "fcnbSecondDominant:D1 "; _
        "gNoteX, gAfterX="; gNoteX; gAfterX; " ";
    For I = 1 To 4
        Debug.Print I; NoteName(I); " "; NoteLetter(I); _
            NoteIndex(I);:
    Next I
    Debug.Print
    Return
    
'*****************************************

' In key of G major, if RootLetter = G
' is the 5th of gAfterX chord, eg C E G C,
' then look for G in gNoteX chord, eg G B D =F,
' and save its voice in RootVoice.
' Will be treatin G as the root of the "unknown"
' chord G B D =F, unknown because not in the
' key signature.

FindgNoteXRoot:
    RootVoice = 0
    For Voice = SOPRANO To BASS ' 1 to 4
        If RootLetter = NoteLetter(Voice) Then
            RootVoice = Voice
            RootIndex = NoteIndex(Voice)
            Exit For
        End If
    Next Voice
    Return
    
'*****************************************
'*****************************************

' If gNoteX is a Secondary Dominant to
' gAfterX, then
'   gosub CheckForDominantChord
' returns pErr7 = "" and
' pVSlash = "V/" if a major triad and
' pVSlash = "V7/" if a major-minor seventh.

CheckSecondDominant:
    bAltered = False
    GoSub InKeySignature
    If Not bAltered Then
        pErr7 = "NotAlt"
    Else
        GoSub CheckForDominantChord
    End If
    Return

'*****************************************
        
' I suspect that the accidental for an
' altered tone, if any, in a chord will
' always be first in the tone, e.g # in #F4,
' but ParseChars searches for it.
' In this example, search for Key$="F#"
' in the key signature.
' If present, then this is not an altered tone
' and gNoteX cannot be a Second Dominant.
' If not present, then this is an altered tone
' and gNoteX may be a Second Dominant.

InKeySignature:
    For Voice = SOPRANO To BASS
        Accid$ = NoteAccid(Voice)
        If Accid$ = "=" _
        Or Accid$ = "x" Then
            bAltered = True
            Exit For
        ElseIf Accid$ = "#" _
        Or Accid$ = "b" Then
            Key$ = NoteLetter(Voice) & NoteAccid(Voice)
            GoSub MatchKeySig
            If Not bInKeySig Then
                bAltered = True
                Exit For
            End If
        End If
    Next Voice
    Return
    
'*****************************************

' Key$ = letter + accidental, eg F#
' If Key$ is in the key signature (original or
' modulated key) of the current note,
' then this is not an altered note, and it
' cannot be a Second Dominant.

MatchKeySig:
    bInKeySig = False
    For I = 1 To gSong(gSongCurr).KeySigUsed
        If Key$ = gSong(gSongCurr).KeySig(I) Then
            bInKeySig = True
            Exit For
        End If
    Next I
    Return
    
'*****************************************

' Chord positions  1  3  5  7 correspond to
' halfsteps                   0 4  7  10
' where chord position 7 is the minor seventh
' of a dominant chord, e.g G B D F is V7

' Eg if Sop=1 and Bas=4
'   1 B4 59  2 =F4 53  3 D4 50  4 G2  31
' the Index2Name's of the chord
'           B4 =F4 D4 G2
' are  59   53  50  31
' where B4 = gNoteName(59).

' Taking G2 as the Root, compute the
' index difference between the Root and
' each of the four tones, modulo 12.
' There are 12 halfsteps in an octave.
' Modulo 12 gives the remainder after
' division by 12.
'   Abs(59-31) mod 12 = 4
'   Abs(53-31) mod 12 = 10
'   Abs(50-31) mod 12 = 7
'   Abs(31-31) mod 12 = 0

' If the remainder = R, then bump the count
' at HalfStep(R) by 1.

' Then check the counts at HalfStep(0, 4, 7, 10).
' Valid chord positions for a major triad are
'   1 1 3 5,  1 3 3 5,  1 3 5 5.   Valid halfsteps are:
'   0 0 4 7,  0 4 4 7,  0 4 7 7.
' Valid chord positions for a major triad and
' minor seventh are (dropping the 5th sometimes)
'   1 3 5  7,    1 1 3  7,   1 3 3  7.    Valid halfsteps are:
'   0 4 7 10,   0 0 4 10, 0 4 4 10.

' Rules for a Major triad or Major-seventh:
' (1) The count for chordpos=7 or halfstep=10
' can be 0 or 1;
' (2) If the count for chordpos=7 is 1, none of
' chordpos=1 or 3 or 5 can be missing;
' (3) If chordpos=7 is missing, none of
' chordpos=1 or 3 or 5 can be missing;
' (4) If chordpos=5 is missing, none of
' chordpos=1 or 3 or 7 can be missing.

CheckForDominantChord:
    For I = 0 To 11
        HalfStep(I) = 0
    Next I
    
    For Voice = SOPRANO To BASS ' 1 to 4
        K = Abs(NoteIndex(Voice) - RootIndex) Mod 12
        HalfStep(K) = HalfStep(K) + 1
        'GoSub Debug2
    Next Voice
    
    pErr7 = ""
    pVSlash = ""
    If HalfStep(10) > 1 Then
        pErr7 = "Dbl7"
    ElseIf HalfStep(10) = 1 Then
        If HalfStep(0) = 0 _
        Or HalfStep(4) = 0 _
        Or HalfStep(7) = 0 Then
            pErr7 = "Not7"
        Else
            pErr7 = ""
            pVSlash = "V7/"
        End If
    ElseIf HalfStep(10) = 0 Then
        If HalfStep(0) = 0 _
        Or HalfStep(4) = 0 _
        Or HalfStep(7) = 0 Then
            pErr7 = "NotT"
        Else
            pErr7 = ""
            pVSlash = "V/"
        End If
    ElseIf HalfStep(7) = 0 Then
        If HalfStep(0) = 0 _
        Or HalfStep(4) = 0 _
        Or HalfStep(10) = 0 Then
            pErr7 = "Not7"
        Else
            pErr7 = ""
            pVSlash = "V7/"
        End If
    End If
    Return
    
'*****************************************

Debug2:
    Debug.Print "fcnbSecondDominant:D2 "; _
        "gNoteX, gAfterX="; gNoteX; gAfterX; " "; _
        "RootIndex RootIndex2 Abs(Diff) K=Mod 12 ="; _
        NoteIndex(Voice); RootIndex; _
        Abs(NoteIndex(Voice)) - RootIndex; K
    Return
    
End Sub

Private Function fcnCand2ndInv(pCh As Integer) _
                            As String
    ' called by sub fcnbCandChord
    ' calls fcnBassStep
    ' calls fcnErrAllow

    ' Globals used: gChord()...
    ' Globals set:

    ' Dim pCh As Integer ' index to gChord of a
    '   potential chord for current gNote(gNoteX).
    Dim Cand1 As Long
    Dim Chord1 As Integer
    Dim Bass1 As Integer
    Dim Index1 As Integer
    Dim OverBass1 As Integer
    Dim Mod1 As Integer

    Dim BC2 As Long ' BestCand of gNote(gAfterX)
    Dim Cand2 As Long
    Dim Chord2 As Integer
    Dim Bass2 As Integer
    Dim Index2 As Integer
    Dim OverBass2 As Integer
    Dim Mod2 As Integer

    Dim I As Integer
    Dim Err As String
    Dim Diff As Integer
    Dim V As Integer
    Dim Chord64 As Integer
    Dim OtherCh As Integer
    Dim RootIndex64 As Integer

    Err = ""
    If gAfterX <= 0 Then
        Exit Function
    End If
    BC2 = gNote(gAfterX).BestCand
    Chord2 = gCand(BC2).Index2Ch

    'If gAfterX = 18 And pCh = 173 Then
    '    I = I ' testing
    'End If
    If gAfterX = 42 Then
        V = V
    End If

    If gChord(pCh).Inversion = SECONDINV Then
        If gAfterX = 42 Then
            V = V
        End If
        If gChord(pCh).bSeventh Then
           'Err = fcnErrAllow(gNoteX, "2Inv") ' call
            GoTo Exit1
        ElseIf gNoteX <= gSong(gSongCurr).FirstNote Then
            Err = fcnErrAllow(gNoteX, "2Inv") ' call
            GoTo Exit1
        Else
            GoSub CheckRest
            If Err <> "" Then
                GoTo Exit1
            End If
            GoSub Middle
        End If
    ElseIf gChord(Chord2).Inversion = SECONDINV Then
        If gAfterX = 42 Then
            V = V
        End If
        If gChord(Chord2).bSeventh Then
           'Err = fcnErrAllow(gNoteX, "2Inv") ' call
            GoTo Exit1
        ElseIf gAfterX <= gSong(gSongCurr).FirstNote Then
            Err = fcnErrAllow(gNoteX, "2Inv") ' call
            GoTo Exit1
        Else
            GoSub Approach
        End If
    End If

Exit1:
    fcnCand2ndInv = Err
    Exit Function

' If one of the next two notes before or after
' gNoteX is a rest (the other may be a bar),
' then flag this 6-4 chord as Err="Rest".
' TODO see gosub CheckForRests as a coding
' example in sub Check_6_4
CheckRest:
    ' TODO check for .firstnote

    If gNote(gNoteX + 1).bRestAll4 Then
        GoTo ErrRest
    ElseIf gNote(gNoteX + 1).bBar Then
        If gNote(gNoteX + 2).bRestAll4 Then
            GoTo ErrRest
        End If
    End If

    If gNote(gNoteX - 1).bRestAll4 _
    Or gNote(gNoteX - 1).bFinalChord Then
        GoTo ErrRest
    ElseIf gNote(gNoteX - 1).bBar Then
        If gNote(gNoteX - 2).bRestAll4 _
        Or gNote(gNoteX - 2).bFinalChord Then
            GoTo ErrRest
        End If
    End If
    Return

ErrRest:
    'Err = "Rest"
    ' Note - you should normally never allow this
    ' error to be bypassed with OP: ALLOW REST
    ' for that would be an invalid 6 4.
    ' It is necessary to go thru fcnErrAllow
    ' for the sake of back-tracking, I think!
    Err = fcnErrAllow(gNoteX, "Rest")
    Return

Middle:
    If gbSmooth Then
        GoTo Exit1
    End If
    ' Case two successive Six-Fours are not allowed
    If gChord(Chord2).Inversion = SECONDINV Then
        Err = fcnErrAllow(gNoteX, "2Inv") ' call
        Return
    End If

    ' Case: pCh is I64 in X-I64-V-I
    ' TODO verify I in I64
    'BC2 = gNote(gAfterX).BestCand
    If gCand(BC2).xHarmProgFlag And HARMPROG_V_I Then
        GoSub VoiceLeading1
        Return
    End If

    ' Case:  pCh is X64 in X-X64-X
    If gChord(pCh).bSeventh Then
        Exit Function
    End If
    Diff = fcnBassStep(pCh) ' call
    Select Case Diff
    Case 9 ' bass note moves more than a whole step
        Err = fcnErrAllow(gNoteX, "2Inv")
        GoTo Exit1
    Case 0 ' Pedal Six-Four
        ' Upper notes usually move by step or less
        GoSub StationaryBass
    Case -1, 1 ' Passing Six-Four
        Chord64 = pCh
        'BC2 = gNote(gAfterX).BestCand
        OtherCh = gCand(BC2).Index2Ch
        GoSub Stationary4th
    End Select
    Return

    ' Case: pCh is I64 in X-I64-V-I
VoiceLeading1:
    Bass1 = gChord(pCh).Voice(BASS).Index2Na
    For V = SOPRANO To TENOR
        Index1 = gChord(pCh).Voice(V).Index2Na
        OverBass1 = (Index1 Mod 12) - (Bass1 Mod 12)
        Select Case OverBass1
            Case MAJORSIXTH, MINORSIXTH, PERFECTFOURTH
                'If gNoteName(Index1) = "A2" Then
                '    V = V ' testing
                'End If
                GoSub ResolveDown
                If UCase$(Err) = "2INV" Then
                    Return
                End If
        End Select
    Next V
    Return

    ' Case: gAfterX is V in X-I64-V-I
    ' The candidate pCh is I64:
    ' (1) does the 6th above the bass resolve
    ' to the 5th above the bass?
    ' (2) does the 4th above the bass resolve
    ' to the 3rd above the bass?

ResolveDown:
    Mod1 = Index1 Mod 12
    Index2 = gChord(Chord2).Voice(V).Index2Na
    Mod2 = Index2 Mod 12
    'If gNoteName(Index1) = "A2" _
    'And gNoteName(Index2) = "C3" Then
    '    V = V ' testing
    'End If
    If Mod1 = Mod2 + 2 _
    Or Mod1 = Mod2 + 1 Then
        Err = ""
    Else
        Err = fcnErrAllow(gNoteX, "2Inv")
    End If
    Return


Approach:
    If gbSmooth Then
        GoTo Exit1
    End If
    ' Case: pCh is X in X-I64-V-I
    'BC2 = gNote(gAfterX).BestCand
    If gCand(BC2).xHarmProgFlag _
    And HARMPROG_I64_V_I Then
        GoSub VoiceLeading2
        Return
    End If

    'If gAfterX = 17 Then
    '    I = I ' testing
    'End If

    ' Case:  pCh is X in X-X64-X Pedal
    If gCand(BC2).xHarmProgFlag _
    And HARMPROG_6_4_Pedal Then
        GoSub StationaryBass
        If UCase$(Err) <> "2INV" Then
            OtherCh = pCh
            'BC2 = gNote(gAfterX).BestCand
            Chord64 = gCand(BC2).Index2Ch
            GoSub NonStationary4th
        End If
        Return
    End If

    ' Case:  pCh is X in X-X64-Y Passing
    If gCand(BC2).xHarmProgFlag _
    And HARMPROG_6_4_Passing Then
        Diff = fcnBassStep(pCh)
        Select Case Diff
        Case -1, 1 ' Passing Six-Four
            OtherCh = pCh
            'BC2 = gNote(gAfterX).BestCand
            Chord64 = gCand(BC2).Index2Ch
            GoSub RisingLine
            If UCase$(Err) = "2INV" Then
                Return
            End If
            GoSub Stationary4th
        Case Else ' 9 or 0
            Err = fcnErrAllow(gNoteX, "2Inv")
        End Select
    End If
    Return

' The bass note in the I64 is not approached
' by the same tone or from the leading tone.
' Ottman p 185.
VoiceLeading2:
    Bass2 = gChord(Chord2).Voice(BASS).Index2Na
    Bass1 = gChord(pCh).Voice(BASS).Index2Na
    If Bass1 = Bass2 Then
        Err = fcnErrAllow(gNoteX, "2Inv")
    ElseIf (Bass1 - gKeyIndex) Mod 12 = _
        MAJORSEVENTH Then ' leading tone
        Err = fcnErrAllow(gNoteX, "2Inv")
    Else
        Err = ""
    End If
    Return


    ' Pedal Six-Four
    ' The Bass is stationary.  (Maybe some other
    ' voice can be instead, like Soprano.)
    ' The other voices usually move by step
    ' or less.  I will let normal 4Part smoothing
    ' take care of that.
StationaryBass:
    Bass1 = gChord(pCh).Voice(BASS).Index2Na
    'BC2 = gNote(gAfterX).BestCand
    'Chord2 = gCand(BC2).Index2Ch
    Bass2 = gChord(Chord2).Voice(BASS).Index2Na
    If Bass1 = Bass2 Then
        Err = ""
    Else
        Err = fcnErrAllow(gNoteX, "2Inv")
    End If
    Return

    ' In a Passing Six-Four group, the Bass notes
    ' should lie in a straight line rising or falling.
RisingLine:
    Bass1 = gChord(pCh).Voice(BASS).Index2Na
    Bass2 = gChord(Chord2).Voice(BASS).Index2Na
    If gCand(BC2).xHarmProgFlag _
    And HARMPROG_6_4_RisingBass Then
        If Bass1 < Bass2 Then ' rising line
            Err = ""
        Else
            Err = fcnErrAllow(gNoteX, "2Inv")
        End If
    Else ' falling line
        If Bass1 > Bass2 Then
            Err = ""
        Else
            Err = fcnErrAllow(gNoteX, "2Inv")
        End If
    End If
    Return


    ' Passing Six-Four
    ' The 4th (RootIndex of the chord) above the bass
    ' in second inversion (the C in G-C-E) usually
    ' is stationary in all three notes (not checked
    ' in gbSmooth cases. Justification: Ottman p 177).
    ' Usually the 5th of the chord is doubled in
    ' a Six-Four, so there is only one RootIndex.
Stationary4th:
    RootIndex64 = -1
    For V = SOPRANO To TENOR
        If gChord(Chord64).Voice(V).ChordPos = _
        Root Then ' ROOT = FIRST = 1 in triad
            RootIndex64 = gChord(Chord64).Voice(V).Index2Na
            GoSub BothEqual
            Return
        End If
    Next V
    Err = fcnErrAllow(gNoteX, "2Inv")
    Return

BothEqual:
    If RootIndex64 = gChord(OtherCh).Voice(V).Index2Na _
    Then
        Err = ""
    Else
        Err = fcnErrAllow(gNoteX, "2Inv")
    End If
    Return

' ***********************************
    ' Pedal Six-Four
    ' The 4th (RootIndex of the chord) above the bass
    ' in second inversion (the C in G-C-E) usually
    ' is not stationary because the 4th resolves down
    ' to the 3rd above the bass.  Check that the Root of
    ' th 6-4 chord is a step or half step above the
    ' other chord.
    ' (Not checked in gbSmooth cases.)
    ' Justification: Ottman p 174 and Fig 9.27b
    ' comments p 186.).
    ' Usually the 5th of the chord is doubled in
    ' a Six-Four, so there is only one RootIndex.
NonStationary4th:
    RootIndex64 = -1
    For V = SOPRANO To TENOR
        If gChord(Chord64).Voice(V).ChordPos = _
        Root Then ' ROOT = FIRST = 1 in triad
            RootIndex64 = gChord(Chord64).Voice(V).Index2Na
            GoSub NonEqual
            Return
        End If
    Next V
    Err = fcnErrAllow(gNoteX, "2Inv")
    Return

NonEqual:
    I = gChord(OtherCh).Voice(V).Index2Na
    If RootIndex64 = I + 1 _
    Or RootIndex64 = I + 2 Then
        Err = ""
    Else
        Err = fcnErrAllow(gNoteX, "2Inv")
    End If
    Return

' ***********************************
' TODO - NOT USED:
   'For I = SOPRANO To BASS
   '    If gNote(gNoteX + 1).Voice(I).xRest = REST1 Then
   '        GoTo ErrRest
   '    ElseIf gNote(gNoteX + 1).bBar Then
   '        If gNote(gNoteX + 2).Voice(I).xRest = REST1 Then
   '            GoTo ErrRest
   '        End If
   '    End If
   '    If gNote(gNoteX - 1).Voice(I).xRest = REST1 Then
   '        GoTo ErrRest
   '    ElseIf gNote(gNoteX - 1).bBar Then
   '        If gNote(gNoteX - 2).Voice(I).xRest = REST1 Then
   '            GoTo ErrRest
   '        End If
   '    End If
   'Next I

End Function


Private Function fcnErr(pText As String) As String
    ' called by sub fcnbCandChord
    ' called by sub ChoosePrevChord
    
    ' Allow err code that begin with a paren
    ' to be treated as ok.
    If Left$(pText, 1) = "(" Then
        fcnErr = ""
    Else
        fcnErr = pText
    End If
End Function

Private Function fcnbInputLine(pFileNo As Integer, _
                gTextLine As String) _
                As Boolean
    ' called by sub InputLines
    ' used with sub PushBackLine
    Dim Err As String

    fcnbInputLine = True
    If gInputStackNo > 0 Then
        gTextLine = gInputStack(gInputStackNo)
        gInputStackNo = gInputStackNo - 1
    Else
        If Not EOF(1) Then ' read 1 line til end of file
            ' Read entire line into variable gTextLine.
            Line Input #pFileNo, gTextLine
            gInputLineNo = gInputLineNo + 1
        Else ' signal end of this song or EOF
            If gbAbcInputVoice Then
                gTextLine = "E:."
            Else
                fcnbInputLine = False
            End If
        End If
    End If

End Function

Private Function fcnGetAbcCh(pVoice As Integer, _
                pChar As String) _
                As String
    ' called by sub ParseAbcNote
    ' called by sub GetAbcString

    ' Works with sub PushAbcCh

    ' Like in "Software Tools",
    ' return a byte both in pChar and as the
    ' function value (for testing)
    '   fcnGetAbcCh=pChar
    ' Return a byte from the buffer if not
    ' empty, else return a byte from the
    ' string and update the .Pos in the string.
    ' If at the end of string, return
    '   fcnGetAbcCh=EOS
    ' where EOS=X'FF', and set .Pos=MAXINTEGER=32767
    Dim BufNum As Integer
    Dim I As Integer

    If gAbcBuf(pVoice).Num > 0 Then
        BufNum = gAbcBuf(pVoice).Num
        pChar = Chr(gAbcBuf(pVoice).xByte1(BufNum))
        gAbcBuf(pVoice).Num = _
            gAbcBuf(pVoice).Num - 1
        GoSub Debug2
    Else
        With gAbcInput(pVoice)
            If .Pos < Len(.String) Then
                .Pos = .Pos + 1
                pChar = Mid$(.String, .Pos, 1)
            Else
                pChar = EOS ' &HFE
                .Pos = MAXINTEGER ' end of string =
            End If
            GoSub Debug1
        End With
    End If
    fcnGetAbcCh = pChar
        If pChar = "|" Then
            I = I
        End If
    Exit Function
    
Debug1:
    'If pVoice <> 1 Then
        Return
    'End If
    If pChar = Chr$(9) Then ' TAB
        Debug.Print "fcnGetAbcCh: pvoice, char at pos="; _
          pVoice; "/"; "TAB"; "/"; gAbcInput(pVoice).Pos
    Else
        Debug.Print "fcnGetAbcCh: pvoice, char at pos="; _
          pVoice; "/"; pChar; "/"; gAbcInput(pVoice).Pos
    End If
    If gAbcInput(pVoice).Pos > 20 Then
        I = I
    End If
    Return
      
Debug2:
    'If pVoice <> 1 Then
        Return
    'End If
    If pChar = Chr$(9) Then ' TAB
        Debug.Print "fcnGetAbcCh: pvoice, buffer char="; _
          pVoice; "/"; "TAB"; "/"
    Else
        Debug.Print "fcnGetAbcCh: pvoice, buffer char="; _
          pVoice; "/"; pChar; "/"
    End If
End Function

Private Sub PushBackLine(gTextLine As String, _
                pErr As String)
    ' called by sub InputLines
    ' Used with fcnbInputLine
    
    If gInputStackNo < MAXINPUTSTACK Then ' 10
        pErr = ""
        gInputStackNo = gInputStackNo + 1
        gInputStack(gInputStackNo) = gTextLine
    Else
        pErr = "FULL"
        gInputErrs = gInputErrs + 1
        Debug.Print "PushBackLine: reached "; _
            "MAXINPUTSTACK = "; MAXINPUTSTACK; _
            ". cannot push back line="; _
            gTextLine
        'Stop
    End If

End Sub
Private Sub PushAbcCh(pVoice As Integer, _
                pChar As String, _
                pbErr As Boolean)

    ' called by sub ParseAbcNote
    ' called by sub GetAbcString

    ' Works with function fcnGetAbcCh

    ' Push a char back on the gAbcBuf,
    ' for fcnGetAbcCh to get later.
    Dim I As Integer
    Dim xByte2 As Byte
    Dim BufNum As Integer

    ' Function CByte converts a number between
    ' 0-255 into a byte for byte arithmetic.
    ' I use it to store a pChar as a byte  in a table
    ' like in "Software Tools".
    If gAbcBuf(pVoice).Num < MAXABCBUF Then ' =80
        gAbcBuf(pVoice).Num = _
            gAbcBuf(pVoice).Num + 1
        BufNum = gAbcBuf(pVoice).Num
        xByte2 = CByte(Asc(pChar))
        gAbcBuf(pVoice).xByte1(BufNum) = xByte2
        pbErr = False
    Else
        GoSub Err1
    End If
    GoSub Debug1
        'If pChar = "|" Then
        '    I = I
        'End If
    Exit Sub
    
Debug1:
    'If pChar <> "|" Then
    '    Return
    'End If
    'If pVoice <> 1 Then
        Return
    'End If
    Debug.Print "PushAbcCh: push back char="; _
        "/"; pChar; "/"
    Return

Err1:
    pbErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "PushAbcCh: reached "; _
        "MAXABCBUF = "; MAXABCBUF; _
        ". cannot push back char="; _
        pChar
    I = I ' testing
    Stop
    Return

End Sub

Private Function fcnNoteIndex(pNote As Integer, _
            pVoice As Integer, _
            pbErr As Boolean) _
            As Integer
    ' called by sub fcnbCandChord
    ' (called by sub FlagNHTHang - stub)
    ' called by sub Harmonize
    
    ' (Allow err code that begin with a paren
    ' to be treated as ok. ??)
    
    ' In handling non-harmonic tones (NHT), some
    ' notes are prolonged from a previous chord.
    ' This function returns the .Index2Name of the
    ' last gNoteX that has a positive entry for this
    ' voice.
    ' The sub StoreTM has stored an offset in
    '   gNote(pNote).Voice(pVoice).Time.Offset
    '  to that OffsetNote in the Tn: entry for this voice.
    
    ' (This means that this routine is only effective
    ' with option
    '   OP: MISC    NHTINPUT    ON
    ' Probably not true. 27sep08 )

    ' If a Voice is commented out for a note, but
    ' its time is specified, then fcnNoteIndex = 0 for it.
    ' 24oct08

    Dim Offset1 As Integer
    Dim I As Integer
    Dim J As Integer
    Dim OffsetNote As Integer
    Dim Ch As Integer

    GoSub Test1
    ' .Index2Name can be pos or neg.
    ' See the gosub MatchInputNotes in fcnbCandChord
    ' TODO this first IF is probably redundant
    If gNote(pNote).Voice(pVoice).Index2Name > 0 Then ' 23oct08
        fcnNoteIndex = _
            gNote(pNote).Voice(pVoice).Index2Name
    ElseIf gNote(pNote).Voice(pVoice).Name <> "z" Then
        Offset1 = _
            gNote(pNote).Voice(pVoice).Time.Offset
            'gNote(pNote).Voice(pVoice).Time.Pos
        If Offset1 >= 0 Then ' is offset1 ever positive?
            fcnNoteIndex = _
                gNote(pNote).Voice(pVoice).Index2Name
                If fcnNoteIndex = 0 Then
                    'OffsetNote = pNote ' 14nov08
                    'GoSub GetMissingVoiceFromBestCand
                ElseIf fcnNoteIndex < 0 Then
                    Stop ' 14nov08 testing
                End If
        Else ' Offset1 < 0 = true offset to a prev note
            OffsetNote = pNote + Offset1 ' = pos + neg
            ' The OffsetNote must have a positive .Pos
            J = gNote(OffsetNote).Voice(pVoice).Time.Pos
            If J >= 0 Then
                fcnNoteIndex = gNote(OffsetNote) _
                    .Voice(pVoice).Index2Name
                If fcnNoteIndex = 0 Then
                    GoSub GetMissingVoiceFromBestCand
                End If
                'Exit Function
            Else
                GoSub Err1
                pbErr = True
                Stop
            End If
        End If
    End If
    Exit Function

'*********************************************

GetMissingVoiceFromBestCand:
    With gNote(OffsetNote)
        If .BestCand > 0 Then
            Ch = gCand(.BestCand).Index2Ch
            If Ch > 0 Then
                fcnNoteIndex = gChord(Ch).Voice(pVoice) _
                     .Index2Na
                If fcnNoteIndex <= 0 Then
                    J = J
                    Stop
                End If
            End If
        Else
            J = J
        End If
    End With
    Return
    
'*********************************************

Test1:
    If gSongCurr = 1 Then
        If gNoteX = 2 Then
            J = J
        End If
    End If
    Return
    
'*********************************************

Err1:
    pbErr = True
    Debug.Print "fcnNoteIndex: internal error, "; _
        " invalid offset in"; _
        " gNote(OffsetNote).Voice(pVoice).Time.Offset="; _
        gNote(OffsetNote).Voice(pVoice).Time.Offset; _
        "for gNoteX="; gNoteX; " and Voice="; pVoice; _
        "in song="; gSongCurr
    Return

End Function

Private Function fcnbParallel(pCh As Integer, _
                            pCh2 As Integer, _
                            pErr As String) _
                            As Boolean
    ' called by sub fcnbCandChord

    ' (Calls function fcnbAdjustThird)
    ' calls function fcnErrAllow

    ' Globals used: gNoteX
    '       gChord()...
    ' Globals set: gCandLast =,
    '       gNote(gNoteX).FirstCand =,
    '       gNote(gNoteX).LastCand =
    '       gCand(gCandLast).Index2Ch =

    ' pCh - index to gChord of a
    '   candidate chord for current gNote(gNoteX).
    ' pCh2 As Integer ' index to gChord of the best
    '   candidate chord for prev gNote(gAfterX).

    ' Returns Err="P5" or "P8" if parallel 5th,8th
    ' Returns Err="Sim4" if all four voices
    '   move up or down together in similar
    '   motion.
    ' Returns Err = "BxT","TxA","AxS" if one
    '   voice crosses another in next chord.
    '   E.g. BxT means that B in gAfterX chord
    '   rises higher than T in the gNoteX chord.
    ' Returns Err="Hid 8" or "Hid5" if parallel
    '   octaves or parallel perfect fifths
    '   by contrary motion. Ottman p ?
    ' Returns Err="P4Int" if Alto or Tenor has an
    '   interval greater than P4 or Perfect 4th.

    Const s As Integer = SOPRANO ' 1
    Const A As Integer = ALTO '    2
    Const T As Integer = TENOR '   3
    Const B As Integer = BASS '    4

    Dim I As Integer
    Dim V As Integer
    Dim V1(SOPRANO To BASS) As Integer ' gNoteX
    Dim V2(SOPRANO To BASS) As Integer ' gAfterX
    Dim V1Incr(SOPRANO To BASS) As Integer
    Dim V2Incr(SOPRANO To BASS) As Integer

    Dim S1 As Integer, S2 As Integer
    Dim A1 As Integer, A2 As Integer
    Dim T1 As Integer, T2 As Integer
    Dim B1 As Integer, B2 As Integer
    Dim Z$
    Dim KeyName As String
    Dim KeyNote As String
    Dim KeyIndex As Integer

    Dim Dif2(1 To 6) As Integer
    Dim Dif1(1 To 6) As Integer
    Dim Same(1 To 6) As Integer
    Dim bParallelOctave As Boolean

    'If gAfterX = 36 And gFile = "Chorale" Then
    'If gAfterX = 26 And gFile = "Assn55" Then
    If gNoteX = 13 Then
        'GoSub Debug1
        I = I
    End If
    GoSub GetCurrIndexes
    GoSub GetAfterIndexes
    'If gAfterX = 36 And gFile = "Chorale" Then
    'If gAfterX = 26 And gFile = "Assn55" Then
        'GoSub Debug2
    'End If
    GoSub GetDifferences
    pErr = ""
    GoSub Parallel
    GoSub Similar
    GoSub Crossing
    GoSub P4Interval
    Exit Function

'******************************************

GetCurrIndexes:
    ' Info for the voices of the chord pCh of
    ' the current candidate for the current
    ' note, gNoteX.
    For V = s To B ' SOPRANO To BASS
        V1(V) = gChord(pCh).Voice(V).Index2Na
    Next V
    Return

'******************************************

GetAfterIndexes:
    ' Info for the voices of the chord pCh2 of
    ' the next candidate to the right, gAfterX,
    ' of the current note, gNoteX.
    For V = s To B
        V2(V) = gChord(pCh2).Voice(V).Index2Na
    Next V
    Return

'******************************************

GetDifferences:
    ' See \Schuman\chorale.txt re the changing
    ' of keys K2: back to K1:.
    ' In fcnbParallel, make every reference
    ' to index to gNoteName to be the offset
    ' to gKeyIndex, e.g.
    '   index-C0 in C major,
    '   index-G0 in G Major.
    ' This allows consistency between key
    ' changes in checking between adjacent
    ' chords for Hid5, Sim4, Bxt, etc.

    With gNote(gAfterX)
        'If .SongKey = gSongKey Then ' 22mar08 use this???
        If gSongKey = gSongKey Then
            S1 = V1(s) ' gNoteX
            A1 = V1(A)
            T1 = V1(T)
            B1 = V1(B)

            S2 = V2(s) ' gAfterX
            A2 = V2(A)
            T2 = V2(T)
            B2 = V2(B)
            If gAfterX = 36 And gFile = "Chorale" Then
                Debug.Print "fcnbParallel:D4 s1,a1,t1,b1="; _
                    S1; A1; T1; B1
                Debug.Print gNoteName(S1); gNoteName(A1); _
                    gNoteName(T1); gNoteName(B1)
                Debug.Print "fcnbParallel:D5 s2,a2,t2,b2="; _
                    S2; A2; T2; B2
                Debug.Print gNoteName(S2); gNoteName(A2); _
                    gNoteName(T2); gNoteName(B2)
                I = I
            End If
        Else
            S1 = V1(s) - gKeyIndex ' gNoteX
            A1 = V1(A) - gKeyIndex
            T1 = V1(T) - gKeyIndex
            B1 = V1(B) - gKeyIndex

            I = .SongKey ' gAfterX
            ' Add a blank to fixed string *2
            ' gsong().key().name so that
            ' it can be trimmed to a normal string.
            Z$ = gSong(gSongCurr).Key(I).Name ' eg C#
            KeyName = Trim$(Z$ & " ")
            KeyNote = KeyName & "0" ' eg C#0
            ' Use function fcnToIx to return the index of
            ' the note, e.g. G#0, in gNoteName.
            KeyIndex = fcnToIx(KeyNote)
            S2 = V2(s) - KeyIndex ' gAfterX
            A2 = V2(A) - KeyIndex
            T2 = V2(T) - KeyIndex
            B2 = V2(B) - KeyIndex
        End If
    End With

    ' TODO what happens if I allow BxT voice crossing?

    ' Assumes sop > alt > ten > bass in chord
    ' pCh or pCh2.
    Dif2(1) = S2 - B2
    Dif2(2) = A2 - B2
    Dif2(3) = T2 - B2
        Dif2(4) = S2 - T2
        Dif2(5) = A2 - T2
    Dif2(6) = S2 - A2

    Dif1(1) = S1 - B1
    Dif1(2) = A1 - B1
    Dif1(3) = T1 - B1
        Dif1(4) = S1 - T1
        Dif1(5) = A1 - T1
    Dif1(6) = S1 - A1

    ' If the bottom (or top) note of the 5th or 8th
    ' interval is the same pitch or octaves
    ' apart, then it is stationary 5th or
    ' 8th, which is acceptable.
    Same(1) = Abs(B2 - B1)
    Same(2) = Abs(B2 - B1)
    Same(3) = Abs(B2 - B1)
        Same(4) = Abs(T2 - T1)
        Same(5) = Abs(T2 - T1)
    Same(6) = Abs(A2 - A1)
    Return

'******************************************

Parallel:
    ' TODO  Ottman p 119-120
    ' Exception: at the final cadence of a
    ' composition or at the end of one of
    ' the major sections, the soprano and
    ' bass commonly display the
    ' octave by contrary motion.
   'pErr = ""
    fcnbParallel = False
    For I = 1 To 6
        ' reject if parallel perfect 5ths
        ' in the same two voices change pitches
        ' in successive chords.
        ' stationary parallel perfect 5ths are ok.
        ' Remember in this program seven half-steps
        ' from 0 to 7 represent the 5th interval
        ' in the traditional triad 1-3-5 where
        ' 1-8 is the octave.
        ' Also 12 half-steps represent an octave.
        If Dif2(I) Mod 12 = 7 Then
            If Dif1(I) Mod 12 = 7 Then
                If Same(I) Mod 12 <> 0 Then
                    If gNoteX = 5 Then
                        I = I
                    End If
                    fcnbParallel = True
                    'GoSub Debug2
                    pErr = fcnErrAllow(gNoteX, "P5")
                   'Exit Function
                   'Exit For
                End If
            ElseIf I = 1 Then
                bParallelOctave = False
                GoSub Hidden
               'Exit For
            End If
        ElseIf Dif2(I) Mod 12 = 0 Then
            If Dif1(I) Mod 12 = 0 Then
                If Same(I) Mod 12 <> 0 Then
                    If Not gNote(gAfterX).bFinalChord _
                    Then ' 12jul08
                         fcnbParallel = True
                         pErr = fcnErrAllow(gNoteX, "P8")
                        'Exit Function
                        'Exit For
                    End If
                End If
            ElseIf I = 1 Then
                bParallelOctave = True
                GoSub Hidden
               'Exit For
            End If
        End If
    Next I
    'fcnbParallel = False
    Return

'******************************************

    ' A hidden (or direct) octave occurs
    ' when two voices progress in similar
    ' motion to a perfect octave.  Although
    ' not parallel, they often produce the
    ' aural effect of parallel movement.
    ' The same for a hidden fifth progressing
    ' to a perfect fifth.
    ' Hidden octaves and fifths are a
    ' problem only when they occur between
    ' the outside voices of a composition
    ' (soprano and bass in four-part writing).
    ' Even then, they are acceptable when
    ' (1) the soprano moves by step, as in
    ' the perfect authentic cadence, soprano
    ' 2^-1^ with bass descending, or
    ' (2) when the bass moves by the interval
    ' of an octave, considered the same as
    ' a repeated tone.  Ottman pp 188-9.

    ' The similar motion in sop and bass
    ' means this is a hidden 5th or 8th.

    ' TODO do we need to distinguish between
    ' two cases: C4 to B3 = 1 halfstep
    ' D4 to C4 = 2 halfsteps?.
Hidden:
    If (S2 > S1 And B2 > B1) _
    Or (S2 < S1 And B2 < B1) Then
        If Abs(S2 - S1) <= 2 Then
            Return
        ElseIf (B2 - B1) Mod 12 = 0 Then
            Return
        Else
           'GoTo HiddenErr
            GoSub HiddenErr
        End If
    End If
    Return

HiddenErr:
    If bParallelOctave Then
        pErr = fcnErrAllow(gNoteX, "Hid8")
    Else
        ' TODO backout this:
        pErr = fcnErrAllow(gNoteX, "Hid5")
    End If
   'Exit Function
    Return

'******************************************

Similar:
    ' TODO 11/19/6
    If pCh = 446 Or pCh = 465 Then
        I = I
    End If

    ' If all four voices rise or all fall,
    ' this is undesirable similar motion.
    ' Bach allows it as the first chord.
    If (S2 > S1 And A2 > A1 _
        And T2 > T1 And B2 > B1) _
    Or (S2 < S1 And A2 < A1 _
        And T2 < T1 And B2 < B1) Then
        ' TODO backout this:
        pErr = fcnErrAllow(gNoteX, "Sim4")
       'Exit Function
    End If
    Return

'******************************************

Crossing:
    ' Crossing voices should be avoided but
    ' are not entirely unusable. Ottman p 188
    ' The only crossing error codes in
    ' array gErrNameLC are "BxT", "TxA" and "AxS",
    ' because they use 3 out of the 31 bits
    ' available in the LONG variables
    '    gxErrorAllowed As Long
    '    gxErrorAllowedPass1 As Long
    '    gxCandErrorBits As Long
    ' That is, if you specify
    '   OP: ALLOW BxT TxA AxS
    ' then you are also allowing
    '             TxB,AxT,SxA.
    If B2 > T1 Then
        pErr = fcnErrAllow(gNoteX, "BxT")
    ElseIf T2 > A1 Then
        pErr = fcnErrAllow(gNoteX, "TxA")
    ElseIf A2 > S1 Then
        pErr = fcnErrAllow(gNoteX, "AxS")
    ' If the higher voice falls below the
    ' lower voice, then modify the pErr field
    ' so that sub
    ' will reverse the letters
    ' in the error displayed on the upper grid,
    ' e.g. T2 < B1 means that the second Tenor
    ' note has fallen below the first Bass note.
    ElseIf T2 < B1 Then
        pErr = fcnErrAllow(gNoteX, "BxT")
        pErr = pErr & "=" & "TxB"
    ElseIf A2 < T1 Then
        pErr = fcnErrAllow(gNoteX, "TxA")
        pErr = pErr & "=" & "AxT"
    ElseIf S2 < A1 Then
        pErr = fcnErrAllow(gNoteX, "AxS")
        pErr = pErr & "=" & "SxA"
        'pErr = fcnReplace(pErr, "BxT", "TxB")
        'pErr = fcnReplace(pErr, "TxA", "AxT")
        'pErr = fcnReplace(pErr, "AxS", "SxA")
    End If
    Return

'******************************************

P4Interval:
    ' Per written comment in Ottman p 90,
    ' should avoid intervals greater than P4,
    ' or Perfect 4th, in Alto and Tenor.
    If Abs(A2 - A1) > 5 Or Abs(T2 - T1) > 5 Then
        If gNoteX = 12 Then
            I = I
        End If
        pErr = fcnErrAllow(gNoteX, "P4Int")
    End If
    Return

' NOT USED: TODO
    ' For information only.  I have not programmed
    ' the following. 04may08
    ' In any place in a phrase, the third of V
    ' may skip by he interval P4
    ' to the third of I, or the reverse;
    ' the remaining voice is stationary.
    ' This skip usually appears in the soprano or
    ' tenor voice.  Each triad includs all notes
    ' of the triad.  This alternative procedure
    ' is an effective way to change from open
    ' to close position or vice versa. Ottman p 95.

'******************************************

Debug1:
    Debug.Print "fcnbParallel:D1 pch;pch2;gafterx="; _
        pCh; pCh2; gAfterX
    Call DebugChord(pCh)
    Call DebugChord(pCh2)
    I = I
    Return
    
'******************************************

Debug2:
    Debug.Print "fcnbParallel:D2 pch;pch2;gafterx="; _
        pCh; pCh2; gAfterX
    Call DebugChord(pCh)
    Call DebugChord(pCh2)
    For I = 1 To 4
        Debug.Print "v1()="; V1(I); gNoteName(V1(I)); ", ";
    Next I
    Debug.Print
    For I = 1 To 4
        Debug.Print "v2()="; V2(I); gNoteName(V2(I)); ", ";
    Next I
    Debug.Print
    I = I
    Return
    
'******************************************

Debug3:
    Debug.Print "fcnbParallel:D3 "; gNoteX; gAfterX; _
    I; Dif1(I); Dif1(I) Mod 12; " / "; _
    Dif2(I); Dif2(I) Mod 12; " / "; _
    Same(I); Same(I) Mod 12
    GoSub Debug1
    GoSub Debug2
    I = I
    Stop
    Return
    
End Function


Private Function fcnbAdjustThird( _
                pNoteX As Integer, _
                pCh As Integer, _
                pVoice As Integer, _
                pIncr As Integer, _
                pErr As String) _
                As Boolean
    ' called by sub fcnbParallel

    ' Globals used: gNote(pNoteX).ThirdIncr,
    '   gChord(pCh).Quality,
    '   gChord(pCh).ScaleDeg,
    '   gScale(MAJOR).RNumName(Sdeg)
    ' Globals set:

    ' Given the index to the notename,
    ' add or subtract one or more sharps
    ' or flats depending on pIncr, which
    ' was set from gNote(gNoteX).AlterAboveBass(x),
    ' where the figured bass symbol, e.g.
    ' #3 or b6, identifies the note above
    ' the bass note to be raised or lowered
    ' a half step.
    Dim SDeg As Integer

    ' If can not raise or
    ' lower the third of this
    ' chord, then flag as an error
    ' and later just display
    ' the unaltered note names.
    fcnbAdjustThird = False
    If gNote(pNoteX).ThirdIncr = INCRSHARP Then
        If gChord(pCh).Quality = QUALMINOR Then
            fcnbAdjustThird = True
        Else
            ' TODO where is this used?
            pErr = "~#3"
        End If
    ElseIf gNote(pNoteX).ThirdIncr = INCRFLAT Then
        If gChord(pCh).Quality = QUALMAJOR Then
            fcnbAdjustThird = True
        Else
            ' TODO where is this used?
            pErr = "~b3"
        End If
    End If
End Function


Private Function fcnbHarmProgOk(pxChordFlag As Byte, _
                            pxCandFlag As Byte) _
                            As Boolean
    ' called by sub fcnbCandChord
    ' calls fcnGetNoteBefore (compare to fcnNoteBefore)
    
    ' Globals used:
    ' Globals set: gChordLast,[many gChord values],

    ' Rule on use of iii6 and vi6.  Each of the
    ' triads usually appears in first inversion
    ' only when the root of the preceding triad
    ' is the same bass note as the first-inversion
    ' note.  There are just two practical
    ' possibilities: I-vi6 and V-iii6. Ottman p 305.
    Dim RN As Integer

    ' The pxChordFlag is from gChord(...).xHarmProgFlag,
    ' which is set only in Sub EditChord for a
    ' Mediant or Submediant chord.
    fcnbHarmProgOk = False
    If pxChordFlag And HARMPROG_V_iii6 Then
        If fcnGetNoteBefore = 0 Then ' firstnote
            fcnbHarmProgOk = True
            Exit Function
        End If
        RN = gNote(gBeforeX).RomNum
        ' Exit if RN was input but <> 5
        If RN > 0 And RN <> 5 Then
            Exit Function ' must be 5 or V
        ' Exit if FB was input but <> root
        ElseIf gNote(gBeforeX).Inversion > _
        ROOTPOS Then
            Exit Function ' must be root pos
        Else
            pxCandFlag = HARMPROG_V_iii6
        End If
    ElseIf pxChordFlag And HARMPROG_I_vi6 Then
        ' Exit if RN was input but <> 1
        If fcnGetNoteBefore = 0 Then ' firstnote
            fcnbHarmProgOk = True
            Exit Function
        End If
        If gNote(gBeforeX).RomNum > TONIC Then
            Exit Function ' must be 1 or I or i
        ' Exit if FB was input but <> root
        ElseIf gNote(gBeforeX).Inversion > _
        ROOTPOS Then
            Exit Function ' must be root pos
        Else
            pxCandFlag = HARMPROG_I_vi6
        End If
    End If
    fcnbHarmProgOk = True
    Exit Function
End Function
Private Function fcnGetNoteBefore() _
                As Integer
    ' called by sub fcnbHarmProgOk
    ' called by sub cmdNextSolution_Click()
    
    ' Compare with Function fcnNoteBefore

    ' Globals used: gNoteX,
    '   gNote(gNoteX).bFirstChord
    '   gNote(gNoteX).bPrevChord
    ' Globals set: gBeforeX
    Dim N As Integer
    Dim FirstNote As Integer
    Dim LastNote As Integer

    FirstNote = gSong(gSongCurr).FirstNote
    If gNoteX = FirstNote Then
        gBeforeX = 0
    ElseIf gNote(gNoteX).bFirstChord Then
        gBeforeX = 0
    Else
        For N = gNoteX - 1 To FirstNote Step -1
            If gNote(N).bPrevChord Then
                gBeforeX = N
                Exit For
            ' TODO next test probably not needed
            ElseIf gNote(N).bFirstChord Then
                gBeforeX = N
                Exit For
            End If
        Next N
    End If
    fcnGetNoteBefore = gBeforeX
End Function

Private Sub CalcVariance()
   ' called by sub PrevTypeChord
    ' calls sub CalcContrary
    
    ' Globals used: gNoteX; gAfterX
    '   gNote(gNoteX)..., gChord(Chord).Voice...
    ' Globals set: gChord(Chord).Variance = SDev

    ' Note I calculate the standard deviation
    ' (SDev) but I label the result as the "variance"
    ' for ease of labelling.

    ' Calculate the SQR of the variances of the
    ' S-A-T indexes between the current
    ' gNoteX=N and the following one, gAferX=A.
    ' Var = SQR( (N.S-A.S)^2 + (N.A-A.A)^2 +
    '           (N.T-A.T)^2 )
    ' S=Soprano, A=Alto, T=Tenor.
    ' We are not using the BASS values for this.
    
    ' OuterVar and OuterSDev and .OuterVariance,
    ' between the soprano and bass, are used to
    ' process chords in contrary, oblique
    ' or similar or stationary motion.

    ' InnerVar and InnerSDev and .InnerVariance,
    ' between the soprano and bass, are used to...
        
    ' If this cand for gNoteX has the same scale
    ' degree step as gAfterX, e.g. V-V or v-V,
    ' flag it as a .bDupe in sub CalcVariance.
    ' If the variance between them is less than
    '   gMinVarianceForDupes
    ' then subtract this cand's variance (and rank)
    ' from 25 to discourage but not prevent the
    ' boring use of near dupe chords, especially
    ' when the program creates 3 or 4 voices 18mar08

    Dim Sop1 As Integer
    Dim Alt1 As Integer
    Dim Ten1 As Integer
    Dim Bas1 As Integer
    Dim Sop2 As Integer
    Dim Alt2 As Integer
    Dim Ten2 As Integer
    Dim Bas2 As Integer
    Dim Var As Single
    Dim SDev As Single
    Dim OuterVar As Single
    Dim OuterSDev As Single
    Dim InnerVar As Single
    Dim InnerSDev As Single
    
    Dim Cand As Long
    Dim Chord As Integer
    Dim BCand2 As Long
    Dim BChord2 As Integer
    Dim bExit As Boolean
    Dim E$
    Dim Voice As Integer
    Dim Voice2 As Integer
    Dim Ix1 As Integer
    Dim Ix2 As Integer
    Dim Offset1 As Integer

'*********************************************
'*********************************************
Main:
    GoSub Test1
    GoSub Init
    If Not bExit Then
        GoSub MainLine
    End If
    gNote(gNoteX).NumContraryOblique = _
        gNumOuterContrary + gNumOuterOblique
    'GoSub Debug1
    Exit Sub

'*********************************************
'*********************************************

    ' TODO not needed 11/28/6
Debug1:
    With gNote(gNoteX)
        If .LastCand > .FirstCand Then
            Debug.Print "CalcVariance: ";
            For Cand = .FirstCand To .LastCand
                Debug.Print _
                gCand(Cand).OuterGraphic; " "; _
                gCand(Cand).InnerGraphic; "   ";
            Next Cand
            Debug.Print
        End If
    End With

'********************************************

Test1:
    If gSongCurr = 1 Then
        If fcnGridNote(gNoteX) >= 13 _
        And fcnGridNote(gNoteX) <= 13 Then
            gNoteX = gNoteX
        End If
    End If
    Return

'********************************************

Init:
    ' TODO 11/26/6 added
    Var = 0
    SDev = 0
    OuterVar = 0
    OuterSDev = 0
    If gNoteX <= 0 Or gNoteX > gNoteLast Then
        Debug.Print "CalcVariance: "; _
            "invalid gNoteX="; gNoteX
        bExit = True
    ElseIf gAfterX <= 0 Or gAfterX > gNoteLast Then
        Debug.Print "CalcVariance: "; _
            "invalid gAfterX="; gAfterX
        bExit = True
    End If
    If gNote(gNoteX).bRestAll4 Then
        Var = Var
    End If

    ' TODO - insurance, may not be needed:
    If gNote(gAfterX).bBar Then
        Stop
    End If
    
    gxOuterMotion = 0
    gxInnerMotion = 0
    gNumOuterContrary = 0
    gNumOuterOblique = 0
    gNumOuterSimilar = 0
    gNumOuterStationary = 0
    gNumInnerContrary = 0
    gNumInnerOblique = 0
    gNumInnerSimilar = 0
    gNumInnerStationary = 0
    'gOuterIndex = 0
    Return

'*********************************************

    ' TODO
    ' If gNote(gAfterX).SongKey <>
    '    gNote(gNoteX).SongKey then
    ' must use gChord(gAfterX).Voice(SOPRANO).name
    ' and use fcnToIx to get the corresponding
    ' index in the other key's chord table.
    ' If a notename doesn't exist in both chord
    ' table, add or subtract an # or b to find the
    ' fit.
MainLine:
    GoSub AfterNotes
    If bExit Then
        Return
    End If
    
    With gNote(gNoteX)
        If .LastCand <= 0 Then
            Exit Sub
        End If
        'N = gNoteX - gSong(gSongCurr).FirstNote
        'txtNoteNo.Text = Str$(N + 1)
        For Cand = .FirstCand To .LastCand
            Chord = gCand(Cand).Index2Ch ' gChord index
            Sop1 = gChord(Chord).Voice(SOPRANO).Index2Na
            Alt1 = gChord(Chord).Voice(ALTO).Index2Na
            Ten1 = gChord(Chord).Voice(TENOR).Index2Na
            Bas1 = gChord(Chord).Voice(BASS).Index2Na
            If Sop1 <= 0 Or Alt1 <= 0 _
            Or Ten1 <= 0 Or Bas1 <= 0 Then ' 29oct08
                'GoSub Err1 ' 26nov08
                'bExit = True
                'Stop ' not expected
                GoTo NextC ' 26nov08
            End If
        
            If gChord(Chord).ScaleDeg = _
            gChord(BChord2).ScaleDeg Then
                gCand(Cand).bDupe = True
            End If
            gCand(Cand).OuterGraphic = ""
            gCand(Cand).InnerGraphic = ""
            GoSub CalcVar
            GoSub CalcNHTDelayedVar
            GoSub CalcOuterVar
            GoSub CalcInnerVar
            Call CalcContrary(Cand, _
                Sop1, Sop2, Alt1, Alt2, _
                Ten1, Ten2, Bas1, Bas2)
NextC:
        Next Cand
    End With
    Return
    
'*********************************************

' If the TM: and T1: to T4; has made this a
' NHTHang note, but if no input tone was specified
' for it, as indicated by
'   bNHTDelayed = true
' then see if the missing tone provided by this
' Cand is the same pitch as the note before or
' after it.  In that case, add 20 to the Variance
' of this note to make it less likely to be selected.

CalcNHTDelayedVar:
    With gNote(gNoteX)
        If .bNHTDelayed Then
            For Voice = SOPRANO To BASS
                Offset1 = .Voice(Voice).Time.Offset
                If Offset1 >= 0 Then
                    Ix1 = gChord(Chord). _
                        Voice(Voice).Index2Na
                    Ix2 = gChord(BChord2). _
                        Voice(Voice).Index2Na
                    If Ix1 = Ix2 Then
                        gCand(Cand).Variance = _
                            gCand(Cand).Variance + 20#
                        Exit For
                    End If
                End If
            Next Voice
        End If
    End With
    Return

'*********************************************

AfterNotes:
    BCand2 = gNote(gAfterX).BestCand ' best candidate
    ' for this gNote(gAfterX) note.
    If BCand2 <= 0 Then
        bExit = True
     Else
        BChord2 = gCand(BCand2).Index2Ch ' gChord index
        Sop2 = gChord(BChord2).Voice(SOPRANO).Index2Na
        Alt2 = gChord(BChord2).Voice(ALTO).Index2Na
        Ten2 = gChord(BChord2).Voice(TENOR).Index2Na
        Bas2 = gChord(BChord2).Voice(BASS).Index2Na
        If Sop2 <= 0 Or Alt2 <= 0 _
        Or Ten2 <= 0 Or Bas2 <= 0 Then
            GoSub Err2
            bExit = True
            Stop ' not expected
        End If
    End If
    Return

'*********************************************

CalcVar:
    Var = (Sop1 - Sop2) ^ 2 + _
            (Alt1 - Alt2) ^ 2 + _
            (Ten1 - Ten2) ^ 2
    SDev = Sqr(Var)
    SDev = Int(SDev * 10# + 0.5) / 10 ' round
    ' the "variance" to one decimal place
    gCand(Cand).Variance = SDev
    If gCand(Cand).bSmooth Then
        ' Force the choice of the SMOOTH chord
        ' progression by dividing the Variance
        ' by two.  Add .01 as another decimal
        ' place to flag this case for eyeballs.
        gCand(Cand).Variance = SDev / 2# + 0.01
    End If
    ' If dupe scaledeg's, and its variance is
    ' less than gMinVarianceForDupes,
    ' then subtract this cand's variance (and rank)
    ' from 25 to discourage the boring use of near
    ' dupe chords, especially when the program
    ' creates three or four voices. 18mar08
    If gCand(Cand).bDupe Then
        If gCand(Cand).Variance <= _
        gMinVarianceForDupes Then
            gCand(Cand).Variance = _
            20! - gCand(Cand).Variance
        End If
    End If
    Return

'*********************************************

CalcOuterVar:
    OuterVar = (Sop1 - Sop2) ^ 2 + _
            (Bas1 - Bas2) ^ 2
    OuterSDev = Sqr(OuterVar)
    OuterSDev = Int(OuterSDev * 10# + 0.5) / 10 ' round
    ' the "variance" to one decimal place
    gCand(Cand).OuterVariance = OuterSDev
    
    ' TODO needed? useful?
    If gCand(Cand).bSmooth Then
        ' Force the choice of the SMOOTH chord
        ' progression by dividing the OuterVariance
        ' by two.  Add .01 as another decimal
        ' place to flag this case for eyeballs.
        gCand(Cand).OuterVariance = OuterSDev / 2# + 0.01
    End If
    Return

'*********************************************

CalcInnerVar:
    InnerVar = (Alt1 - Alt2) ^ 2 + _
            (Ten1 - Ten2) ^ 2
    InnerSDev = Sqr(InnerVar)
    InnerSDev = Int(InnerSDev * 10# + 0.5) / 10 ' round
    ' the "variance" to one decimal place
    gCand(Cand).InnerVariance = InnerSDev
    
    ' TODO needed? useful?
    If gCand(Cand).bSmooth Then
        ' Force the choice of the SMOOTH chord
        ' progression by dividing the InnerVariance
        ' by two.  Add .01 as another decimal
        ' place to flag this case for eyeballs.
        gCand(Cand).InnerVariance = InnerSDev / 2# + 0.01
    End If
    Return

'*********************************************

Err1:
    E$ = "CalcVariance:E1 gSongCurr=" & gSongCurr _
        & " gNoteX=" & gNoteX _
        & " cand=" & Cand _
        & " chord=" & Chord & " has a zero note in SATB=" _
        & Sop1 & " " & Alt1 & " " & Ten1 & " " & Bas1
    Debug.Print E$
    Call DisplayText(E$)
    Return
    
'*********************************************

Err2:
    E$ = "CalcVariance:E2 gSongCurr=" & gSongCurr _
        & "gNoteX=" & gNoteX _
        & " cand=" & Cand _
        & " bChord2=" & BChord2 & " has a zero note in SATB=" _
        & Sop2 & " " & Alt2 & " " & Ten2 & " " & Bas2
    Debug.Print E$
    Call DisplayText(E$)
    Return

End Sub

Private Sub CalcContrary(pCand As Long, _
                        pSop1 As Integer, _
                        pSop2 As Integer, _
                        pAlt1 As Integer, _
                        pAlt2 As Integer, _
                        pTen1 As Integer, _
                        pTen2 As Integer, _
                        pBas1 As Integer, _
                        pBas2 As Integer)
    ' called by sub CalcVariance
    ' Globals used:
    ' Globals set:

    ' From note gNoteX to gAfterX, if the Soprano
    ' rises then
    ' (1) if the Bass rises
    ' call this Similar Motion.
    ' (2) if the Bass is stationary,
    ' call this Oblique Motion.
    ' (3) if the Bass falls,
    ' call this Contrary Motion.

    ' From note gNoteX to gAfterX, if the Soprano
    ' falls then
    ' (1) if the Bass rises
    ' call this Contrary Motion.
    ' (2) if the Bass is stationary,
    ' call this Oblique Motion.
    ' (3) if the Bass falls,
    ' call this Similar Motion.

    ' If both Soprano and Bass are stationary,
    ' call this Stationary Motion.

    ' If Oblique Motion or Stationary Motion,
    ' use tight smoothing, from sub CalcVariance.

    ' If Similar Motion, find the variance (SDev)
    ' of only the voices moving in that similar
    ' direction.
    ' TODO. Including the Bass if not stationary?
    
    ' If Contrary Motion, find the variance (SDev)
    ' of only the voices moving in that similar
    ' direction.

    ' Compare the biggest variance of the Similar
    ' Motion chords with the biggest variance of
    ' the Contrary Motion chords.  Choose the chord
    ' showing the biggest movement, either a Similar
    ' Motion chord or a Contrary Motion chord.

    ' Note I calculate the standard deviation
    ' (SDev) but I label the result as the "variance"
    ' for ease of labelling.

    ' Calculate the SQR of the variances of the
    ' S-A-T indexes between the current
    ' gNoteX=N and the following one, gAferX=A.
    ' Var = SQR( (N.S-A.S)^2 + (N.A-A.A)^2 +
    '           (N.T-A.T)^2 )
    ' S=Soprano, A=Alto, T=Tenor.
    ' We are not using the BASS values.
    
    'Dim gxOuterMotion as byte
    'Dim xMotionForm As Byte
    'Dim xMotionValue As Byte

    Dim Var As Single
    Dim SDev As Single
    Dim Cand As Long
    Dim Chord As Integer
    Dim BCand2 As Integer
    Dim BChord2 As Integer

    Dim bExit As Boolean
    Dim A$
    Dim E$
    Dim Outer$
    Dim Inner$

'*********************************************

Main:
    GoSub Init
    GoSub MainLine
    'Debug.Print "CalcContrary:"; gNoteX; gNumOuterContrary; _
    '    gNumOuterOblique
    Exit Sub
    
'*********************************************

Init:
    Return
    
'*********************************************

MainLine:
    GoSub CalcOuterMotion
    GoSub CalcInnerMotion
    gCand(pCand).xOuterMotion = gxOuterMotion ' S & B
    gCand(pCand).xInnerMotion = gxInnerMotion ' A & T
    gCand(pCand).OuterGraphic = Outer$ ' S & B
    gCand(pCand).InnerGraphic = Inner$ ' S & B
    Return

'*********************************************
'*********************************************

CalcOuterMotion:
    gxOuterMotion = ZEROBYTE
    If pSop1 > pSop2 Then
        If pBas1 < pBas2 Then
            gxOuterMotion = gxOuterMotion Or CONTRARY
            gNumOuterContrary = gNumOuterContrary + 1
            gxOuterMotion = gxOuterMotion Or CONVERGING
            'Outer$ = ">"
            Outer$ = "DU"
        ElseIf pBas1 = pBas2 Then
            gxOuterMotion = gxOuterMotion Or OBLIQUE
            gNumOuterOblique = gNumOuterOblique + 1
            gxOuterMotion = gxOuterMotion Or CONVERGING
            gxOuterMotion = gxOuterMotion Or FALLING
            'Outer$ = "_\"
            Outer$ = "D="
        Else ' If pBas1 > pBas2 Then
            gxOuterMotion = gxOuterMotion Or Similar
            gNumOuterSimilar = gNumOuterSimilar + 1
            gxOuterMotion = gxOuterMotion Or FALLING
            'Outer$ = "\\"
            Outer$ = "DD"
        End If
    ElseIf pSop1 < pSop2 Then
        If pBas1 > pBas2 Then
            gxOuterMotion = gxOuterMotion Or CONTRARY
            gNumOuterContrary = gNumOuterContrary + 1
            gxOuterMotion = gxOuterMotion Or DIVERGING
            'Outer$ = "<"
            Outer$ = "UD"
        ElseIf pBas1 = pBas2 Then
            gxOuterMotion = gxOuterMotion Or OBLIQUE
            gNumOuterOblique = gNumOuterOblique + 1
            gxOuterMotion = gxOuterMotion Or DIVERGING
            gxOuterMotion = gxOuterMotion Or RISING
            'Outer$ = "/_"
            Outer$ = "U="
        Else ' If pBas1 < pBas2 Then
            gxOuterMotion = gxOuterMotion Or Similar
            gNumOuterSimilar = gNumOuterSimilar + 1
            gxOuterMotion = gxOuterMotion Or RISING
            'Outer$ = "//"
            Outer$ = "UU"
        End If
    Else ' If pSop1 = pSop2 Then
        If pBas1 > pBas2 Then
            gxOuterMotion = gxOuterMotion Or OBLIQUE
            gNumOuterOblique = gNumOuterOblique + 1
            gxOuterMotion = gxOuterMotion Or DIVERGING
            gxOuterMotion = gxOuterMotion Or FALLING
            'Outer$ = "\-"
            Outer$ = "=D"
        ElseIf pBas1 = pBas2 Then
            gxOuterMotion = gxOuterMotion Or STATIONARY
            gNumOuterStationary = gNumOuterStationary + 1
            Outer$ = "="
        Else ' If pBas1 < pBas2 Then
            gxOuterMotion = gxOuterMotion Or OBLIQUE
            gNumOuterOblique = gNumOuterOblique + 1
            gxOuterMotion = gxOuterMotion Or CONVERGING
            gxOuterMotion = gxOuterMotion Or RISING
            'Outer$ = "-/"
            Outer$ = "=U"
        End If
    End If
    Return

'*********************************************

CalcInnerMotion:
    gxInnerMotion = ZEROBYTE
    If pAlt1 > pAlt2 Then
        If pTen1 < pTen2 Then
            gxInnerMotion = gxInnerMotion Or CONTRARY
            gNumInnerContrary = gNumInnerContrary + 1
            gxInnerMotion = gxInnerMotion Or CONVERGING
            'Inner$ = ">"
            Inner$ = "DU"
        ElseIf pTen1 = pTen2 Then
            gxInnerMotion = gxInnerMotion Or OBLIQUE
            gNumInnerOblique = gNumInnerOblique + 1
            gxInnerMotion = gxInnerMotion Or CONVERGING
            gxInnerMotion = gxInnerMotion Or FALLING
            'Inner$ = "_\"
            Inner$ = "D="
        Else ' If pTen1 > pTen2 Then
            gxInnerMotion = gxInnerMotion Or Similar
            gNumInnerSimilar = gNumInnerSimilar + 1
            gxInnerMotion = gxInnerMotion Or FALLING
            'Inner$ = "\\"
            Inner$ = "DD"
        End If
    ElseIf pAlt1 < pAlt2 Then
        If pTen1 > pTen2 Then
            gxInnerMotion = gxInnerMotion Or CONTRARY
            gNumInnerContrary = gNumInnerContrary + 1
            gxInnerMotion = gxInnerMotion Or DIVERGING
            'Inner$ = "<"
            Inner$ = "UD"
        ElseIf pTen1 = pTen2 Then
            gxInnerMotion = gxInnerMotion Or OBLIQUE
            gNumInnerOblique = gNumInnerOblique + 1
            gxInnerMotion = gxInnerMotion Or DIVERGING
            gxInnerMotion = gxInnerMotion Or RISING
            'Inner$ = "/_"
            Inner$ = "U="
        Else ' If pTen1 < pTen2 Then
            gxInnerMotion = gxInnerMotion Or Similar
            gNumInnerSimilar = gNumInnerSimilar + 1
            gxInnerMotion = gxInnerMotion Or RISING
            'Inner$ = "//"
            Inner$ = "UU"
        End If
    Else ' If pAlt1 = pAlt2 Then
        If pTen1 > pTen2 Then
            gxInnerMotion = gxInnerMotion Or OBLIQUE
            gNumInnerOblique = gNumInnerOblique + 1
            gxInnerMotion = gxInnerMotion Or DIVERGING
            gxInnerMotion = gxInnerMotion Or FALLING
            'Inner$ = "\-"
            Inner$ = "=D"
        ElseIf pTen1 = pTen2 Then
            gxInnerMotion = gxInnerMotion Or STATIONARY
            gNumInnerStationary = gNumInnerStationary + 1
            Inner$ = "="
        Else ' If pTen1 < pTen2 Then
            gxInnerMotion = gxInnerMotion Or OBLIQUE
            gNumInnerOblique = gNumInnerOblique + 1
            gxInnerMotion = gxInnerMotion Or CONVERGING
            gxInnerMotion = gxInnerMotion Or RISING
            'Inner$ = "-/"
            Inner$ = "=U"
        End If
    End If
    Return

End Sub

Private Function fcnbMaxSATBVarianceOver(pChord As Integer, _
                                pLimit As Integer) _
                                As Boolean
    ' called by sub fcnbCandChord
    ' called by sub fcnCand2ndInv
    ' calls:

    ' Globals used: pChord; gAfterX
    '   gNote(pChord)..., gChord(I).Voice...
    ' Globals set: gChord(I).Variance = SDev

    ' If the difference in the index value of any
    ' S=Soprano, A=Alto, T=Tenor, B=Bass
    ' voice between pChord and gAfterX is greater
    ' than pLimit (1= half-step, 2=whole step),
    ' then return
    '   fcnMaxSATBVariance=false

    Dim Sop1 As Integer
    Dim Alt1 As Integer
    Dim Ten1 As Integer
    Dim Bas1 As Integer
    Dim Sop2 As Integer
    Dim Alt2 As Integer
    Dim Ten2 As Integer
    Dim Bas2 As Integer
    Dim Var As Single
    Dim SDev As Single
    Dim I As Integer
    Dim Cand As Long
    Dim Best As Integer
    Dim BestX As Integer
    Dim MaxVar As Integer

    fcnbMaxSATBVarianceOver = False
    If pChord <= 0 Or pChord > gChordLast Then
        Debug.Print "fcnbMaxSATBVarianceOver: "; _
            "invalid pChord="; pChord
        Exit Function
    ElseIf gAfterX <= 0 Or gAfterX > gNoteLast Then
        Debug.Print "fcnbMaxSATBVarianceOver: "; _
            "invalid gAfterX="; gAfterX
        Exit Function
    End If

    ' TODO - insurance, may not be needed:
    If gNote(gAfterX).bBar Then
        Stop
    End If

    ' If any voice of the after chord is a rest, then
    ' stop. Insurance.
    For I = SOPRANO To BASS
        If gNote(gAfterX).Voice(I).xRest = REST1 Then
            Stop ' insurance
        End If
    Next I

    Best = gNote(gAfterX).BestCand ' best candidate
    ' for this gNote(gAfterX) note.
    If Best <= 0 Then
        Exit Function
    Else
        BestX = gCand(Best).Index2Ch ' gChord index
        Sop2 = gChord(BestX).Voice(SOPRANO).Index2Na
        Alt2 = gChord(BestX).Voice(ALTO).Index2Na
        Ten2 = gChord(BestX).Voice(TENOR).Index2Na
        Bas2 = gChord(BestX).Voice(BASS).Index2Na
    End If
    If Sop2 <= 0 Or Alt2 <= 0 _
    Or Ten2 <= 0 Or Bas2 <= 0 Then
        Exit Function
    End If

    MaxVar = 0
    Sop1 = gChord(pChord).Voice(SOPRANO).Index2Na
    Alt1 = gChord(pChord).Voice(ALTO).Index2Na
    Ten1 = gChord(pChord).Voice(TENOR).Index2Na
    Bas1 = gChord(pChord).Voice(BASS).Index2Na
    If Sop1 = Sop2 _
    And Alt1 = Alt2 _
    And Ten1 = Ten2 _
    And Bas1 = Bas2 Then
        fcnbMaxSATBVarianceOver = True ' reject
        Exit Function
    End If

    If Abs(Sop1 - Sop2) > pLimit Then
        fcnbMaxSATBVarianceOver = True
        Exit Function
    ElseIf Abs(Alt1 - Alt2) > pLimit Then
        fcnbMaxSATBVarianceOver = True
        Exit Function
    ElseIf Abs(Ten1 - Ten2) > pLimit Then
        fcnbMaxSATBVarianceOver = True
        Exit Function
    ElseIf Abs(Bas1 - Bas2) > pLimit Then
        fcnbMaxSATBVarianceOver = True
        Exit Function
    End If

End Function

Private Function fcnbMaxSATVarianceOver(pChord As Integer, _
                                pLimit As Integer) _
                                As Boolean
    ' called by sub fcnbCandChord
    ' called by sub fcnCand2ndInv ?
    ' calls:

    ' Globals used: pChord; gAfterX
    '   gNote(pChord)..., gChord(I).Voice...

    ' If the difference in the index value of any
    ' S=Soprano, A=Alto, T=Tenor, B=Bass
    ' voice between pChord and gAfterX is greater
    ' than pLimit (1= half-step, 2=whole step),
    ' then return
    '   fcnMaxSATBVariance=false

    Dim Sop1 As Integer
    Dim Alt1 As Integer
    Dim Ten1 As Integer
    Dim Bas1 As Integer
    Dim Sop2 As Integer
    Dim Alt2 As Integer
    Dim Ten2 As Integer
    Dim Bas2 As Integer
    Dim Var As Single
    Dim SDev As Single
    Dim I As Integer
    Dim Cand As Long
    Dim Best As Integer
    Dim BestX As Integer
    Dim MaxVar As Integer

    fcnbMaxSATVarianceOver = False
    If pChord <= 0 Or pChord > gChordLast Then
        Debug.Print "fcnbMaxSATVarianceOver: "; _
            "invalid pChord="; pChord
        Exit Function
    ElseIf gAfterX <= 0 Or gAfterX > gNoteLast Then
        Debug.Print "fcnbMaxSATVarianceOver: "; _
            "invalid gAfterX="; gAfterX
        Exit Function
    End If

    ' TODO - insurance, may not be needed:
    If gNote(gAfterX).bBar Then
        Stop
    End If

    For I = SOPRANO To TENOR
        If gNote(pChord).Voice(I).xRest = REST1 Then
            Stop ' insurance
        End If
    Next I

    ' If any voice of the after chord is a rest, then
    ' stop.  Insurance.
    For I = SOPRANO To TENOR
        If gNote(gAfterX).Voice(I).xRest = REST1 Then
            Stop ' insurance
        End If
    Next I

    Best = gNote(gAfterX).BestCand ' best candidate
    ' for this gNote(gAfterX) note.
    If Best <= 0 Then
        Exit Function
    Else
        BestX = gCand(Best).Index2Ch ' gChord index
        Sop2 = gChord(BestX).Voice(SOPRANO).Index2Na
        Alt2 = gChord(BestX).Voice(ALTO).Index2Na
        Ten2 = gChord(BestX).Voice(TENOR).Index2Na
    End If
    If Sop2 <= 0 Or Alt2 <= 0 _
    Or Ten2 <= 0 Then
        Exit Function
    End If

    MaxVar = 0
    Sop1 = gChord(pChord).Voice(SOPRANO).Index2Na
    Alt1 = gChord(pChord).Voice(ALTO).Index2Na
    Ten1 = gChord(pChord).Voice(TENOR).Index2Na
    If Sop1 = Sop2 _
    And Alt1 = Alt2 _
    And Ten1 = Ten2 Then
        fcnbMaxSATVarianceOver = True ' reject
        Exit Function
    End If

    If Abs(Sop1 - Sop2) > pLimit Then
        fcnbMaxSATVarianceOver = True
        Exit Function
    ElseIf Abs(Alt1 - Alt2) > pLimit Then
        fcnbMaxSATVarianceOver = True
        Exit Function
    ElseIf Abs(Ten1 - Ten2) > pLimit Then
        fcnbMaxSATVarianceOver = True
        Exit Function
    End If

End Function



Private Sub cmdNextFile_Click()

'   called by cmd button "Next file" in upper
'   right of upper grid. 16aug08
'   calls sub Main4Part
'   passing the flag
'       gbFileNameNext=true
'   so that sub Main4Part will skip
'       call InputFileName

' See sub InputFileName
' See sub InputLines

'Dim gbFileNameNext As Boolean
'Const FILENAMESMAX As Integer' = 50
'  Types: eg gDirSongs or gFile or ...
'Dim gFileTypes(FILENAMESMAX) As String
' Names: eg "Asn511a"
'Dim gFileNames(FILENAMESMAX) As String
'(Dim gFileNamesDir As String)
'Dim gFileNamesUsed As Integer
'Dim gFileNamesCurr As Integer

' gbWriteAll
    
' from sub InputFileName
    Dim I As Integer
    Dim Prefix As String
    Dim File As String
    Dim E$
    Dim Type1$
    Dim bEndOfList As Boolean
    Dim bInDirSong As Boolean
    Dim bFoundNext As Boolean
    Dim bReset As Boolean
    Dim SongNo As Integer

    Dim bDebugPrintSave As Boolean
    Dim NumInputStrings As Integer

'***************************************
'***************************************

Main:
    GoSub Init
    GoSub MainLine
    'If bReset Then
     '   gFileNamesCurr = 0
     '   GoSub Mainline
   ' End If
    Exit Sub
    
'***************************************
'***************************************
    
Init:
    txtPrint.Text = ""
    txtMyCand.Text = ""
    gbFileNameNext = True
    Return
    
'***************************************
    
MainLine:
    bInDirSong = False
    If gbWriteAll Then ' set by cmdWriteAll_click
        gFileNamesCurr = 0
        Do While Not bEndOfList  ' Loop until end of list
            GoSub Test1
            GoSub SelectType
            If bFoundNext Then
                GoSub ProcessFile
            End If
        Loop
    Else
        GoSub SelectType
        If bFoundNext Then
            GoSub ProcessFile
        End If
    End If
    Return

'***************************************

Test1:
    If gFile = "Asn511g" Then
        I = I
    End If
    If gFile = "Asn511h" Then
        I = I
    End If
    Return

'***************************************

ProcessFile:
    Call Init4Part
    Call Main4Part
    SongNo = 1 ' remaining songs are gSongLast-1
    While SongNo < gSongLast
        Call cmdNextSong_Click
        GoSub Debug3
        SongNo = SongNo + 1
    Wend
    I = I
    Return
    
'***************************************

SelectType:
    gFile = ""
    bInDirSong = False
    GoSub Bump
    Do While Not bEndOfList  ' Loop until end of list
        Type1$ = gFileTypes(gFileNamesCurr)
        Select Case Type1$
            Case "GDIRSONGS"
                If bInDirSong Then
                    GoSub PushBack
                    Exit Do
                Else
                    'bInDirSong = True
                    gDirSongs = gFileNames(gFileNamesCurr)
                End If
            Case "GFILE"
                If bInDirSong Then
                    GoSub PushBack
                    bInDirSong = False
                    Exit Do
                Else
                    bInDirSong = True
                    gFile = gFileNames(gFileNamesCurr)
                End If
            Case "GABCFILE"
                gAbcFile = gFileNames(gFileNamesCurr)
            Case "G4PARTFILE"
                g4PartFile = gFileNames(gFileNamesCurr)
            Case Else ' eg blank line or xBEGIN
        End Select
        GoSub Bump
    Loop
    If gDirSongs = "" Then
        GoSub Err3
    ElseIf gFile = "" Then
        GoSub Err4
    Else
        bFoundNext = True
        'GoSub Debug2
    End If
    
    Return

'***************************************

Bump:
    If gFileNamesCurr < gFileNamesUsed Then
        gFileNamesCurr = gFileNamesCurr + 1
    ElseIf gFileNamesCurr = gFileNamesUsed Then
        bEndOfList = True
    Else
        GoSub Err1
        bReset = True
    End If
    Return

'***************************************

PushBack:
    'If gFileNamesCurr < gFileNamesCurr Then
    If gFileNamesCurr > 1 Then
        gFileNamesCurr = gFileNamesCurr - 1
    Else
        bEndOfList = True
        GoSub Err2
        'gFileNamesCurr = gFileNamesUsed
    End If
    Return

'**************************************

Err1:
    gInputErrs = gInputErrs + 1
    E$ = "cmdNextFile_Click:E1 " & _
        "reached gFileNamesUsed=" & _
        gFileNamesUsed & _
        "at end of BEGINFILELIST"
    Debug.Print E$
    I = I
    'Stop
    Return

'**************************************

Err2:
    gInputErrs = gInputErrs + 1
    E$ = "cmdNextFile_Click:E2 " & _
        "reached gFileNamesUsed=" & _
        gFileNamesUsed & _
        "at begin of BEGINFILELIST"
    Debug.Print E$
    I = I
    Stop
    Return

'**************************************

Err3:
    gInputErrs = gInputErrs + 1
    E$ = "cmdNextFile_Click:E3 " & _
        " missing gDirSongs at lineno=" & _
        gFileNamesCurr & _
        " in BEGINFILELIST"
    Debug.Print E$
    I = I
    Stop
    Return

'**************************************

Err4:
    gInputErrs = gInputErrs + 1
    E$ = "cmdNextFile_Click:E4  " & _
        " missing gFile at lineno=" & _
        gFileNamesCurr & _
        " in BEGINFILELIST"
    Debug.Print E$
    I = I
    Stop
    Return

'**************************************

Debug1:
    'gInputErrs = gInputErrs + 1 ' not really an error
    E$ = "cmdNextFile_Click:D1 " & _
        "reached FILENAMESMAX=" & _
        FILENAMESMAX & _
        "in BEGINFILELIST"
    Debug.Print E$
    Return
    
'**************************************

Debug2:
    'gInputErrs = gInputErrs + 1 ' not really an error
    E$ = "cmdNextFile_Click:D2 " & _
        "found gGfile=" & gFile & _
        " in BEGINFILELIST"
    Debug.Print E$
    Return
'**************************************

Debug3:
    'gInputErrs = gInputErrs + 1 ' not really an error
    E$ = "cmdNextFile_Click:D3 " & _
        " gGfile=" & gFile & _
        " gsongcurr gsonglast=" & _
        gSongCurr & ", " & gSongLast
    Debug.Print E$
    Return
    
End Sub

Private Sub cmdNextNote_Click()
    ' called by sub Click
    ' Globals used: gSongCurr, gNoteLast,
    '       gSong(gSongCurr), gNote(I).LastCand
    ' Globals set: gNoteCurr =, gNoteX =
    Dim I As Integer

    txtPrint.Text = ""
    txtMyCand.Text = ""
    ' Display the candidate chords for the
    ' next note that has at least one
    ' candidate chord.  Skip over the notes
    ' at a single or double measure bar.
    With gSong(gSongCurr)
        'If gSongCurr = 4 Then Stop
        gNoteCurr = gNoteX
        If gNoteLast > 0 Then
            If gNoteCurr < .LastNote Then
                For I = gNoteCurr + 1 To .LastNote
                    If gNote(I).LastCand > 0 _
                    Then
                        GoTo Found
                    End If
                Next I
                For I = .FirstNote To gNoteCurr - 1
                    If gNote(I).LastCand > 0 _
                    Then
                        GoTo Found
                    End If
                Next I
            Else
                For I = .FirstNote To .LastNote
                    If gNote(I).LastCand > 0 _
                    Then
                        GoTo Found
                    End If
                Next I
            End If
        End If
    End With
    Exit Sub
Found:
    gNoteCurr = I
    gNoteX = gNoteCurr
    Call DisplayCandChords
End Sub

Private Sub cmdNextSolution_Click()
    ' called by Click
    ' called by sub txtMyCand_KeyPress
    ' called by sub SaveRestoreNextSong
    ' called by sub cmdPrevSolution_Click
    
    ' calls ChooseLastChord(gNoteX)
    ' calls ChoosePrevChord(gNoteX)
    ' calls DisplayCandChords 'gNoteX
    ' calls Harmonize
    ' calls DisplayHarm(gSongCurr)
    ' calls function fcnGetNoteBefore
    ' calls Function fcnNoteAfter
    
    ' Globals used: gNote(gNoteX).BestCand
    '       .bFinalChord, .bPrevChord
    ' Globals set: gCand(.BestCand).Error,
    '       gCand(.BestCand).PrevBest,
    '       .nGoodCand, .SolutionNo
    '       gAfterX, gCandLast
    '       cmdShow.Caption =

    Dim I As Integer
    Dim J As Integer
    Dim FirstNote As Integer
    Dim SaveGNoteX As Integer
    Dim SaveBestCand As Long
    Dim SaveBestCand2 As Long
    Dim Err As String
    Dim bFirstTime
    Dim bPrevSolCalledMe As Boolean
    Dim bExit As Boolean
    Dim A$
    Dim E$
    Dim Note As Integer

'*********************************************
'*********************************************
Main:
    GoSub Init
    If Not bExit Then
        GoSub MainLine
    End If
    Exit Sub

'*********************************************
'*********************************************

Init:
    bExit = False
    txtPrint.Text = ""
    txtMyCand.Text = ""
    ' Copied for convenience:
    FirstNote = gSong(gSongCurr).FirstNote

    bFirstTime = True
    Err = ""

    ' TODO allow exception for .UserCand > 0
    ' where .nGoodCand may = 0 ?
    If gNote(gNoteX).nGoodCand <= 1 Then
        'If .UserCand = 0 Then
        If Not gNote(gNoteX).bUserChoice Then
            GoSub Err1
            bExit = True
        End If
    End If
    GoSub Test1
    Return
    
'*******************************************

Test1:
    If gNoteX = 2 Then
        I = I
    End If
    Return
    
'*******************************************

MainLine:
    Do While gNote(gNoteX).nGoodCand > 0
        With gNote(gNoteX)
            If bFirstTime Then
                bFirstTime = False
                ' Flag the current .BestCand
                ' with Soln + its error msg if any
                GoSub UpdateCandSol
            End If
            SaveBestCand = .BestCand
            
            ' Find a new best candidate
            If .bUserChoice Then
                GoSub UserChoice
                GoTo Exit1
            End If
            
            bExit = False ' insurance
            If .bFinalChord Then
                ' returns .BestCand for gNoteX
                Call ChooseLastChord(gNoteX)
                GoSub SavePrevBest
            ElseIf .bPrevChord Then
                ' returns .BestCand for gNoteX
                Call ChoosePrevChord(gNoteX)
                GoSub SavePrevBest
            Else
                GoTo Err3
            End If
            
            GoSub NewBestCandidate
            If bExit Then
                GoTo Exit1
            'Else
            '    GoTo Loop1
            End If
        End With
Loop1:
    Loop
    ' If we fall thru here, there is no solution
    GoSub DisplaySolution
    'GoTo Err1
    GoSub Err1
    
Exit1:
    Return
    'Exit Sub

'*******************************************

UserChoice:
    ' 11/11/06
    ' I think the following comment still applies
    ' but the new logic of using MyCand to
    ' point to an error cand (e.g. P4INT)
    ' eliminates the need for
    ' a negative number in MyCand text box.
    
    ' Need to re-enter .UserCand as a positive
    ' number to backout the negative
    ' .UserCand one.
    
    With gNote(gNoteX)
        If .BestCand = .UserCand Then
            '.UserCand = 0
            GoSub AllowedErrorRank
            GoSub DisplaySolution
            'Exit Sub
            bExit = True
        Else
            .BestCand = .UserCand
            '.UserCand = 0
            'gCand(.BestCand).PrevBest = _
            '    SaveBestCand
            GoSub AllowedErrorRank
            GoSub SavePrevBest
            'If gCand(.BestCand).Error <> "" _
            'Then
                GoSub DisplaySolution
                'Exit Sub
                bExit = True
            'ELSE ???  TODO
            'End If
        End If
    ' TODO
    .bUserChoice = False
    End With
    Return

'*********************************************
        
AllowedErrorRank:
    ' If this is an allowed error, eg (P4int,
    ' do not choose a new .BestCand when
    ' going back, but get the adjusted
    ' rank of the allowed error.
    With gNote(gNoteX)
        If Left$(gCand(.BestCand).Error, 1) = "(" _
        Then
           SaveBestCand2 = .BestCand
           GoSub Rank
           .BestCand = SaveBestCand2
        End If
    End With
    Return

'*********************************************

Rank:
    With gNote(gNoteX)
        If .bFinalChord Then
            ' returns .BestCand for gNoteX
            Call ChooseLastChord(gNoteX)
            'GoSub SavePrevBest
        ElseIf .bPrevChord Then
            ' returns .BestCand for gNoteX
            Call ChoosePrevChord(gNoteX)
            'GoSub SavePrevBest
        Else
            Stop
            'GoTo Err3
        End If
    End With
    Return

'*******************************************

' When only doing a cmdNextSolution,
' save the previous best candidate index in
' gCand(.BestCand).PrevBest, which should = 0.
' But if sub cmdPrevSolution called
' cmdNextSolution, then don't clobber the
' prevbest, since we are going back using it.
SavePrevBest:
    With gNote(gNoteX)
        ' TODO cleanup
        'If gCand(.BestCand).PrevBest = 0 Then
        If Not bPrevSolCalledMe Then
            gCand(.BestCand).PrevBest = _
                SaveBestCand
        End If
    End With
    Return

'*******************************************

NewBestCandidate:
    gAfterX = gNoteX ' for insurance?
    ' TODO 11/11/06
    ' I think that I started to write this sub
    '   call NewBestCandidate
    ' to replace the following code.  Need to check it.
    
    If gNoteX = FirstNote Then
        ' Save a pointer to the previous
        ' best candidate in this one.
        'gCand(.BestCand).PrevBest = _
        '    SaveBestCand
        'GoSub UpdatePrevCand
        gCandLast = gNote(gNoteX).LastCand
        GoSub NHTBAckTracking
        GoSub DisplaySolution
        'Exit Sub
        bExit = True
    Else ' gNoteX > FirstNote
        GoSub Harmonize
        If Err = "NoSol" Then
            gNoteX = SaveGNoteX  ' set at Harmonize
            GoSub UpdateErrorCand
            ' Reset BestCand to saved one
            '.BestCand = SaveBestCand
            'GoSub Harmonize
            Call DisplayCandChords 'gNoteX
            'GoSub DisplaySolution
            ' Remove "Temp"
            'gCand(.BestCand).Error = ""
            'GoTo Loop1 ' TODO not needed?
            bExit = False ' goto Loop1
        Else
            ' Reset gNoteX to
            ' display the candidate chords
            ' at the gNoteX that needed a
            ' new solution, else get the
            ' new next note to the Left$.
            gNoteX = SaveGNoteX ' set at Harmonize
            'gCand(.BestCand).PrevBest = _
            '    SaveBestCand
            'GoSub UpdateCand
            GoSub DisplaySolution
            'Exit Sub
            bExit = True
        End If
    End If
    Return

'*******************************************

' In Sub cmdNextSolution_Click, and then in
' Sub Harmonize,
' it seems inefficient to throw away all candidates
' to the left of the note that you are getting
' the next solution for, and to recalculate
' all the candidates for all the notes to the left.
' In many cases, only one or two notes will be
' changed to the left of the note with the new
' solution. 30oct08

' E.g. let A, B and C be successive notes.
' Get the next solution for C, which means that
' its .BestCand is changed. Then B will
' have some changed candidates, even if it
' keeps the same .BestCand.  Then A will have
' no changes to candidates.

' To avoid the wholesale discarding of the
' candidates to the left, must chain the list
' of Cand's.  In the e.g. above, all of B's
' Cand's would be released and recalculated.
' If B's .BestCand remained the same, then
' none of A's Cand's would be released.

Harmonize:
    ' Recalculate the harmony for
    ' all notes to the left of the
    ' one with the new solution
    ' up to
    '       J = gNoteX - 1

    ' Reset gCandLast to override all
    ' the candidates in the gCand
    ' array for notes to the left of
    ' this one in the upper panel
    ' in order to save gCand space.

    ' TODO verify this. note that gnotex=gafterx
     gCandLast = gNote(gNoteX).LastCand
    'If gNote(gNoteX).FirstCand > 1 Then
    '    gCandLast = fcnGreater( _
    '        gNote(gNoteX).FirstCand, _
    '        gNote(gAfterX).LastCand) - 1
    'End If

    SaveGNoteX = gNoteX
    GoSub NHTBAckTracking
    J = fcnGetNoteBefore ' gNoteX
    'J = gNoteX - 1 ' pass a copy of
    ' gNoteX - 1 to avoid possible
    ' problem where changing
    ' the local parm variable also
    ' changes the global.
    GoSub ResetBestCands ' see gcandlast just above
    Err = ""
    Call Harmonize(FirstNote, _
        J, gAfterX, Err)
    Return
    
'*******************************************

NHTBAckTracking:
    SaveGNoteX = gNoteX
    Note = fcnNoteAfter(gNoteX)
    If Note > 0 Then
        If gNote(Note).xNHT And NHTHANG Then
            Err = ""
            Call Harmonize(Note, Note, gAfterX, Err)
            ' only do once, for sub Harmonize does the rest
        End If
    End If
    gNoteX = SaveGNoteX
    Return
    
'*******************************************

ResetBestCands:
    For I = FirstNote To J
        gNote(I).BestCand = 0
    Next I
    Return
    
'*******************************************

DisplaySolution:
    ' Display the candidates for
    ' this note with the new solution
    Call DisplayCandChords 'gNoteX
    Call DisplayHarm(gSongCurr)
    gbShowHarmony = True
    cmdShow.Caption = _
        "&Show input"
    Return

'*******************************************

' Used if NOSOL
UpdateErrorCand:
    With gNote(gNoteX)
        ' Flag the current best candidate
        ' with the temporary error msg "Sol n"
        ' Allow for errors permitted by option
        ' E.G. OP: ALLOW SIM4 ==> "(SIM4"
        gCand(.BestCand).Error = "^Sol" & _
            Mid$(Str$(.SolutionNo), 2) & _
            RTrim$(gCand(.BestCand).Error)
        .nGoodCand = .nGoodCand - 1
        .SolutionNo = .SolutionNo + 1
        Return
    End With

'*******************************************

' Used first time procedure is entered
UpdateCandSol:
    With gNote(gNoteX)
        ' Flag the current best candidate
        ' solution
        ' with the temporary error msg "Sol" & n

        '    gbCalledbyPrevSol = true
        ' is set in sub cmdPrevSolution
        ' Notice that the local boolean
        '   bPrevSolCalledMe
        ' is a copy of the global boolean
        '   gbCalledbyPrevSol
        ' and that the local one
        ' goes away after the program exits
        ' and gets reset to false on next
        ' entry.
        If gbCalledbyPrevSol Then
            bPrevSolCalledMe = _
                gbCalledbyPrevSol ' local boolean=true
            gbCalledbyPrevSol = False
        Else
            'A$ = gCand(.BestCand).Error
            gCand(.BestCand).Error = "Sol" & _
                Mid$(Str$(.SolutionNo), 2) & _
                RTrim$(gCand(.BestCand).Error)
            ' The following test is now done in
            '   txtMyCand_KeyPress
            ' if MyCand choice, then add 1 to nGoodCand
            'If .bUserChoice Then
            '    If Left$(A$, 1) <> "(" Then
            '        .nGoodCand = .nGoodCand + 1
            '    End If
            'End If
            .nGoodCand = .nGoodCand - 1
            .SolutionNo = .SolutionNo + 1
        End If
        Return
    End With

'*******************************************

Err1:
    E$ = "cmdNextSolution_Click: " _
        & "there is no other 4-part solution " _
        & "to song=" & gSongCurr
    txtPrint.Text = E$
    Debug.Print E$
    'Stop
    'Exit Sub
    Return

'*******************************************

Err2:
    E$ = "cmdNextSolution_Click: " _
        & "gNoteX=" & gNoteX _
        & "should be > FirstNote=" & FirstNote
    txtPrint.Text = E$
    Debug.Print E$
    'Stop
    Exit Sub

'*******************************************

Err3:
    E$ = "cmdNextSolution_Click: " _
        & "should be bFirstChord or bPrevchord"
    Debug.Print E$
    Exit Sub

'*******************************************
' NOT_USED:

Err4:
    Debug.Print "cmdNextSolution_Click: "; _
        "nGoodCand="; _
        gNote(gNoteX).nGoodCand; _
        "should be > 1"
    Stop

' Not used
UpdatePrevCand:
    With gNote(gNoteX)
        ' Flag the current best candidate
        ' with the temporary error msg "Sol n"
        If Err = "" Then
            gCand(SaveBestCand).Error = _
                "Sol" & Mid$(Str$(.SolutionNo), 2)
        Else
            gCand(SaveBestCand).Error = _
                "^Sol" & Mid$(Str$(.SolutionNo), 2)
        End If
        .nGoodCand = .nGoodCand - 1
        .SolutionNo = .SolutionNo + 1
        Return
    End With

' Not used
UpdateCand:
    With gNote(gNoteX)
        ' Flag the current best candidate
        ' with the temporary error msg "Sol n"
        If Err = "" Then
            gCand(SaveBestCand).Error = "Sol" & _
                Mid$(Str$(.SolutionNo), 2)
        Else
            ' Flag the new BestCand as error
            gCand(.BestCand).Error = "^Sol" & _
                Mid$(Str$(.SolutionNo), 2)
            ' Reset BestCand to saved good one
            .BestCand = SaveBestCand
        End If
        .nGoodCand = .nGoodCand - 1
        .SolutionNo = .SolutionNo + 1
        Return
    End With

' Not used
DisplayHarmony:
    Call DisplayHarm(gSongCurr)
    gbShowHarmony = True
    cmdShow.Caption = _
        "&Show input"
    Return

' END_OF_NOT_USED:

End Sub

Private Sub NewBestCandidate()
    ' called by cmdNextSolution_Click
    ' Globals used: gNote(gNoteX).BestCand
    '       .bFinalChord, .bPrevChord
    ' Globals set: gCand(.BestCand).Error,
    '       gCand(.BestCand).PrevBest,
    '       .nGoodCand, .SolutionNo
    '       gAfterX, gCandLast
    '       cmdShow.Caption =

    Dim I As Integer
    Dim J As Integer
    Dim FirstNote As Integer
    Dim SaveGNoteX As Integer
    Dim SaveBestCand As Integer
    Dim Err As String
    Dim E$

    'txtPrint.Text = ""
    ' Copied for convenience:
    FirstNote = gSong(gSongCurr).FirstNote

    ' Recalculate the harmony for
    ' all notes to the Left$ of the
    ' one with the new solution.
    If gNoteX = FirstNote Then
        ' Display the candidates for
        ' this note with the new solution
        Call DisplayCandChords ' gNoteX
        Call DisplayHarm(gSongCurr)
        gbShowHarmony = True
        cmdShow.Caption = _
            "&Show input"
        Exit Sub
    Else ' gNoteX > FirstNote
        ' Reset gCandLast to override all
        ' the candidates in the gCand
        ' array for notes to the Left$ of
        ' this one, to save space.

        If gNote(gNoteX).FirstCand > 1 Then
            If gNote(gNoteX).FirstCand - 1 <> _
                gNote(gAfterX).LastCand Then
                I = I: Stop
            End If
            If gNote(gAfterX).LastCand > 0 Then
                gCandLast = gNote(gAfterX).LastCand
            End If
            'gCandLast = fcnLesser( _
            '    gNote(gNoteX).FirstCand - 1, _
            '    gNote(gAfterX).LastCand)
        End If

        SaveGNoteX = gNoteX
        J = gNoteX - 1 ' pass a copy of
        ' gNoteX - 1 to avoid possible
        ' problem where changing
        ' the local parm variable also
        ' changes the global.
        Err = ""
        Call Harmonize(FirstNote, _
            J, gAfterX, Err)
        If Err = "NoSol" Then
            GoTo Loop1
        Else
            ' Display the candidate chords
            ' at the gNoteX that needed a
            ' new solution, else get the
            ' new next note to the Left$.
            gNoteX = SaveGNoteX
            Call DisplayCandChords 'gNoteX
            Call DisplayHarm(gSongCurr)
            gbShowHarmony = True
            cmdShow.Caption = _
                "&Show input"
            Exit Sub
        End If
    End If
    GoTo Err1
    'Exit Sub

Err1:
    E$ = "NewBestCandidate: " _
        & "there is no 4-part solution " _
        & "to song=" & gSongCurr
    txtPrint.Text = E$
    Debug.Print E$
    'Stop
    Exit Sub

Err2:
    E$ = "cmdNextSolution_Click: " _
        & "gNoteX=" & gNoteX _
        & "should be > FirstNote=" & FirstNote
    txtPrint.Text = E$
    Debug.Print E$
    'Stop
    Exit Sub

Err3:
    E$ = "cmdNextSolution_Click: " _
        & "should be bFirstChord or bPrevchord"
    Debug.Print E$
    'Stop
    Exit Sub

' *************************
' NOT_USED:

Err4:
    Debug.Print "cmdNextSolution_Click: "; _
        "nGoodCand="; _
        gNote(gNoteX).nGoodCand; _
        "should be > 1"
    Stop
' END_OF_NOT_USED:

End Sub
Private Sub cmdPrevSolution_Click()
    ' called by Click
    
    ' calls sub cmdNextSolution_Click
    ' calls sub ChooseLastChord(gNoteX)
    ' calls sub ChoosePrevChord(gNoteX)
    
    ' Globals used: gNote(gNoteX).BestCand
    '       .bFinalChord, .bPrevChord
    ' Globals set: gCand(.BestCand).Error,
    '       gCand(.BestCand).PrevBest,
    '       .nGoodCand, .SolutionNo
    '       gAfterX, gCandLast
    '       cmdShow.Caption =

    Dim I As Integer
    Dim J As Integer
    Dim PrevBest As Integer
    Dim FirstNote As Integer
    Dim LastNote As Integer
    Dim SaveGNoteX As Integer
    Dim SavePrevBest As Integer
    Dim SaveBestCand As Integer
    Dim A$
    Dim B$
    Dim Err As String
    Dim E$

'*********************************************
'*********************************************
Main:
    GoSub Init
    GoSub MainLine
    Exit Sub

'*********************************************
'*********************************************

Init:
    txtPrint.Text = ""
    txtMyCand.Text = ""
    ' Copied for convenience:
    FirstNote = gSong(gSongCurr).FirstNote
    LastNote = gSong(gSongCurr).LastNote
    Return

'*********************************************

MainLine:
    With gNote(gNoteX)
        If .SolutionNo <= 1 Then
            I = I
            GoTo Err1
        End If
        A$ = gCand(.BestCand).Error
        If Left$(A$, 1) <> "^" Then
            .BestCand = _
                gCand(.BestCand).PrevBest
            If .BestCand = 0 Then
                I = I
                Stop
            End If
        End If
        A$ = "TEMP"
        ' skip over ^sol till get to sol
        Do Until A$ = "Sol" Or A$ = ""
            A$ = gCand(.BestCand).Error
            A$ = Left$(A$, 3)
            If A$ = "^So" Then
                GoSub BackUpOneCand
                ' keep on undoing the NoSol ^Soln's
                ' until you get to the first good Soln.
                .BestCand = _
                    gCand(.BestCand).PrevBest
                If .SolutionNo <= 0 Then
                    I = I
                    GoTo Err1
                End If
            ElseIf A$ = "NoS" Then ' NoSol
                I = I
                Stop ' insurance
            ElseIf A$ = "Sol" Then
                GoSub BackUpOneCand
                ' only go back to undo the last good Sol
                ' each time that PrevSolutions is pressed.
            ElseIf Left$(A$, 1) = "(" Then
                ' undo the next Soln
                GoSub BackUpOneCand
                .BestCand = _
                    gCand(.BestCand).PrevBest
            Else
                GoTo Err3
            End If
        Loop
        
        gbCalledbyPrevSol = True
        'GoSub AllowedErrorRank
        Call DisplayCandChords 'gNoteX
        Call cmdNextSolution_Click
        ' TODO not needed? 11/18/6
        Call DisplayHarm(gSongCurr) ' TODO
        gbShowHarmony = True
        cmdShow.Caption = _
            "&Show input"
    End With
    gbCalledbyPrevSol = False
    Return

'*********************************************

BackUpOneCand:
    With gNote(gNoteX)
        ' If .error was "SolN" & "(err" then
        ' strip off the "SolN", else blank it.
        B$ = gCand(.BestCand).Error
        I = InStr(B$, "(")
        If I > 0 Then
            gCand(.BestCand).Error = Mid$(B$, I)
            'gCand(.BestCand).Error = Mid$(B$, I + 1)
            A$ = "" ' terminate the loop
            'A$ = "temp" ' continue the loop once more
            ' to undo the next Soln.
            '.BestCand = gCand(.BestCand).PrevBest
        Else
            gCand(.BestCand).Error = ""
        End If
        If .SolutionNo > 1 Then
            .nGoodCand = .nGoodCand + 1
            .SolutionNo = .SolutionNo - 1
        Else
            I = I
        End If
        'call DisplayCandChords 'gNoteX
    End With
    Return

'*********************************************

Err1:
    E$ = "cmdPrevSolution_Click: " _
        & "there is no previous 4-part solution " _
        & "to song=" & gSongCurr
    txtPrint.Text = E$
    Debug.Print E$
    'Stop
    Exit Sub

'*********************************************

Err3:
    E$ = "cmdPrevSolution_Click: " _
        & "gCand(.BestCand).Error=" _
        & A$ & " does not start with 'Sol' " & _
        "or '^Sol' for gNoteX=" & gNoteX
    txtPrint.Text = E$
    Debug.Print E$
    'Stop
    I = I
    Exit Sub

'*********************************************

Err4:
    E$ = "cmdPrevSolution_Click: " _
        & ".BestCand's .PrevBest's .Error=" _
        & A$ & " does not start with 'Sol' for" _
        & " gNoteX=" & gNoteX
    txtPrint.Text = E$
    Debug.Print E$
    'Stop
    Exit Sub

'*********************************************
    ' TODO not used
    ' Remove the NoSol flag from all candidates.
    'With gNote(gNoteX)
    '    For I = .FirstCand To .LastCand
    '        If Left$(gCand(I).Error, 4) = "^Sol" Then
    '            gCand(I).Error = ""
    '            .nGoodCand = .nGoodCand + 1
    '            .SolutionNo = .SolutionNo - 1
    '        End If
    '    Next I
    'End With

'*********************************************
    ' TODO not used
        ' Find the previous best candidate
        ' and remove it's temporary error
        ' msg "Sol n"
    With gNote(gNoteX)
        For I = 1 To 2
            A$ = gCand(.BestCand).Error
            If Left$(A$, 3) <> "Sol" Then
                GoTo Err4
            End If
            GoSub GoBackOneCand
            PrevBest = gCand(.BestCand).PrevBest
            If PrevBest <= 0 Then
                'GoSub DisplayChords
                'GoTo Err1
            Else
                .BestCand = PrevBest
            End If
        Next I
    End With
' End of not used?

'*********************************************
    ' TODO not used
DisplayChords:
    ' Display the candidates for
    ' this note with the new solution
    Call DisplayCandChords 'gNoteX
    Call DisplayHarm(gSongCurr)
    gbShowHarmony = True
    cmdShow.Caption = _
        "&Show input"
    Return

'*********************************************
' TODO not used
GoBackOneCand:
    With gNote(gNoteX)
        gCand(.BestCand).Error = ""
        If .SolutionNo > 1 Then
            .nGoodCand = .nGoodCand + 1
            .SolutionNo = .SolutionNo - 1
        Else
            I = I
        End If
        Call DisplayCandChords 'gNoteX
        Return
    End With
' END OF NOT USED

End Sub

Private Sub cmdShowAccid_Click()
    ' called by cmd click
    ' Globals used:
    ' Globals set: gbShowKeyAccidentals =,
    '       cmdShowAccid.Caption =

    txtPrint.Text = ""
    If Not gbShowKeyAccidentals Then
        gbShowKeyAccidentals = True
        cmdShowAccid.Caption = "Hi&de key #,b"
    Else
        gbShowKeyAccidentals = False
        cmdShowAccid.Caption = "Show &key #,b"
    End If
    Call DisplayNotes(gSongCurr)
    Call DisplayCandChords
    Call DisplayHarm(gSongCurr)
    'call cmdHarmony_Click
    Exit Sub
    
End Sub

Private Sub cmdValidCand_Click()
    ' called by cmd click
    ' Globals used:
    ' Globals set: gbShowValidCands =,
    '       cmdValidCand.Caption =

    txtPrint.Text = ""
    If Not gbShowValidCands Then
        gbShowValidCands = True
        cmdValidCand.Caption = "&Every cand"
    Else
        gbShowValidCands = False
        cmdValidCand.Caption = "&Valid cands"
    End If
    Call DisplayCandChords
End Sub


Private Sub cmdWriteAbc_Click()
    ' called by click
    ' TODO
    Call WriteAbc(gSongCurr, gSongLast)
End Sub
Private Sub WriteAbc(pSongCurr As Integer, _
        pSongLast As Integer)
    ' called by cmdWriteAbc_Click

    ' calls sub WriteAbcSong
    ' calls sub WriteAbcNotes
    ' calls sub DisplayText
    ' calls sub PrintNotes (commented out)

    ' Globals used: gPrefix, gDirSongs, gFile
    ' Globals set:

    ' Convert the 4 part harmony data, mostly
    ' from the upper grid (because some notes
    ' in lines 1 to 4 may have been adjusted)
    ' to ABCPlus format and write out to
    '    filename.ABC.
    ' The DOS based JEDABC program will read in
    ' the filename.ABC and will invoke programs
    '    abcm2ps (ABC to Postscript) and
    '    abcMidi (ABC to MIDI).

    ' abcm2ps creates a
    '    filename.PS
    ' which the Ghost View program GSview
    '   GSview32.exe
    ' uses to print the score to screen or paper.

    ' abcMidi creates a
    '    filenameN.MID
    ' for every X:n song in
    '    filename.ABC.
    ' Many MIDI playing programs, inluding that
    ' built into windows media player, can play
    ' the polyphonic music in
    '    filenameN.MID

    Dim FileNo As Integer
    Dim Prefix As String
    Dim File As String
    Dim bErr As Boolean
    Dim E$
    Dim LineOut As Integer
    Dim I As Integer
    Dim Song As Integer
    Dim LastSong As Integer
    Dim bFirstSong As Boolean

'*******************************************
'*******************************************

Main:
    GoSub Init
    If bErr Then
        Exit Sub
    End If
    GoSub OpenFile
    If gbWriteAll Then ' set by cmdWriteAll_click
        LastSong = pSongLast
    Else
        LastSong = pSongCurr
    End If
    
    bFirstSong = True
    Call WriteAbcSong(FileNo, LineOut, bFirstSong)
    Call WriteAbcNotes(Song, FileNo, LineOut)
    bFirstSong = False
    
    While gSongCurr < LastSong
        Call cmdNextSong_Click
        Call WriteAbcSong(FileNo, LineOut, bFirstSong)
        Call WriteAbcNotes(gSongCurr, FileNo, LineOut)
    Wend
    GoSub Conclusion
        'call DisplayText(E$)
    Exit Sub

'*******************************************
' END OF Main:
'*******************************************

Init:
    If pSongCurr <= 0 Or pSongCurr > gSongLast Then
        GoSub Err1
    End If
    LineOut = 0
    Return

'*******************************************

OpenFile:
    'gFileOnePrefix = "C:\vb4prg\4part\"
    'gPrefix & gDirSongs are from
    '   gFileOnePrefix  &  \4partN.txt
        
    Prefix = gPrefix & gDirSongs
    File = gFile & ".ABC"
    FileNo = FreeFile

    Open Prefix & File For Output As #FileNo
    E$ = "Opened file=" & Prefix & File
    Call DisplayText(E$)
    Debug.Print E$
    Return

'*******************************************
' END OF Init:
'*******************************************

Conclusion:
    'call PrintNotes(pSongCurr, LineOut)
    Close #FileNo
    E$ = "Closed file=" & Prefix & File & _
        ", # Lines=" & LineOut
    Call DisplayText(E$)
    Debug.Print E$
    Return

'*******************************************

Err1:
    bErr = True
    gInputErrs = gInputErrs + 1
    E$ = "WriteAbc: " & _
        "invalid SongNo=" & pSongCurr
    Call DisplayText(E$)
    Debug.Print E$
    Return
    'Exit Sub

End Sub
Private Sub WriteAbcNotes9(pSongno As Integer, _
                        pFileNo As Integer, _
                        pLineOut As Integer)
    
    '**********************************************
    
    ' NOTICE sub WriteAbcNotes9 is buggy and is
    '       NOT USED. 28sep08
    
    ' Sub WriteAbcNotes9 was my latest
    ' version as of 25sep08 but it had a bug
    ' that I couldn't easily fix.  I regressed
    ' back to the previous version of WriteAbcNotes,
    ' and I updated it with all the features of
    ' the buggy WriteAbcNotes9 that I could
    ' identify.
    ' The Bug was failure to "split and extend"
    ' the notes in the Bass line to match all
    ' the extra notes in the TM: line, as
    ' provided by sub WriteAbcNotesBass4. 28sep08
    
    '**********************************************
    
    
    ' called by WriteAbc

    ' calls sub WriteAbcNotesBass4
    ' calls sub DisplayText
    ' calls sub Note2Abc
    ' calls sub Time2Abc
    ' calls function fcnRepChar
    ' calls function fcnReplace

    ' Globals used: gPrefix, gDirSongs, gFile
    ' Globals set:
    
    ' 12/2/6.
    ' Notice that I cannot bypass calling
    ' sub WriteAbcNotesBass4
    ' even if there are no lyric lines.
    ' The w: lines of Rom Num and Figured Bass.
    ' lines 6 and 7, depend on the bass notes
    ' having the most notes in the measure.
    ' It is a constraint of ABCPlus or abcm2ps.
    ' Possibly I can insert invisible x or y
    ' notes to map the w: fields to.
    
    ' 2/16/8.
    ' Build the output line, Buf$ = Buf$ +,
    ' instead of Print #pFileNo, xxx;
    ' so that I can scan for the "beaming" operator ~,
    ' and remove the spaces between the notes
    ' that are to be beamed in ABC.

    Dim I As Integer
    Dim J As Integer
    Dim nSpaces As Integer
    Dim Row As Integer
    Dim Col As Integer
    Dim SaveRow As Integer
    Dim SaveCol As Integer
    Dim VHdr As Variant ' SOPRANO To BASS
    Dim A$
    Dim B$
    Dim MeterLeng As Integer
    Dim NoteLeng$
    Dim AbcLeng$
    Dim Err As String
    Dim bErr As Boolean
    Dim E$
    Dim BegSlur$
    Dim EndSlur$
    Dim Bass4Tie$
    Dim Rest1Char$
    Dim NumRests As Integer
    Dim bMinus As Boolean
    Dim MinusSign As Integer
    Dim MinusPos As Integer
    Dim NewA$
    Dim NHTPref$
    Dim Buf$
    Dim Tilde$
    ' My Convention: use Const's instead of
    ' constants so that it is easy to
    ' update the row numbers.
    Const ROW4 As Integer = 4
    Const ROW5 As Integer = 5
    Const ROW6 As Integer = 6
    Const ROW16 As Integer = 16
    Const ROW20 As Integer = 20
    Const ROW21 As Integer = 21
    Const ABCColWidth As Integer = 14

    Dim CurrColWidth As Integer
    Dim ABCPos As Integer
    Dim ABCTabPos As Integer
    Dim bWroteKey As Boolean
    Dim RowOrder As Integer
    Dim NumPrintRows As Integer
    Dim RowPrintOrder As Variant
    Dim gbBufCompress As Boolean


'*******************************************
'*******************************************

Main:
    GoSub Init
    GoSub InitRows
    'If bErr Then
    '    Exit Sub
    'End If
    GoSub PrintNoteLoop ' grdTable4P.Row,
                        ' grdTable4P.Col
        'GoSub PrintRowHeaderLoop
                        ' grdTable4P.Row
        'GoSub PrintColLoop ' grdTable4P.Col
            'GoSub FormatColRow1to4
                'GoSub CheckMinusThisCol
                'GoSub BarNotes
                'GoSub SlurNotes
                'call WriteAbcNotesBass4
                'GoSub CheckNHT
                'GoSub ConvNote
                    'call Note2Abc
                    'GoSub NoteLength
                        'GoSub GetNoteTime
                        'call Time2Abc
                        'GoSub Err2
                    'GoSub Err2
            'GoSub FormatColRowOver5
                'GoSub GetNumRests
                'GoSub GetNoteTime
                'GoSub ConvText
                'fcnRepChar
        'GoSub CheckRow3AltoClef
    Exit Sub

'*******************************************
' END OF Main:
'*******************************************

Init:
    VHdr = Array("", "[V: S] ", "[V: A] ", _
                    "[V: T] ", "[V: B] ")
    ' If gbAlto3 then output data for the Alto
    ' voice for printing by abcm2ps in an
    ' individual part Alto clef.
    gbAlto3 = False
    bWroteKey = False
    Return

'*******************************************

InitRows:
    'RowPrintOrder = Array(1, 2, 3, 4, 13, 14, 22, 23, _
        6, 7, 8, 9, 12)
        
    NumPrintRows = 13
    If gbOneLyricLinePerVoice Then
        ' one w: after each row 1-4
        RowPrintOrder = Array(1, 14, 2, 15, _
            3, 23, 4, 24, _
            6, 7, 8, 9, 12)
    Else ' w: after rows 1-4
        RowPrintOrder = Array(1, 2, 3, 4, _
            14, 15, 23, 24, _
            6, 7, 8, 9, 12)
    End If

    If gbShowInputNotesOnly Then
        NumPrintRows = 8
    End If
    'CurrColWidth = ABCColWidth
    Return


'*******************************************
' END OF Init:
'*******************************************

PrintNoteLoop:
    NumRests = 0
    ' Do not print the last row (16) of Chord#'s
    ' and the last four T1: to T4: rows (17-20).
    ' and the last Bass4: row (21).
    'For Row = 0 To grdTable4P.Rows - 1
    For RowOrder = 0 To NumPrintRows - 1
        Row = RowPrintOrder(RowOrder)
        Buf$ = ""
        If (Row >= 0 And Row <= 12) Then
            GoSub PrintRows
        ElseIf gNumLyricLines > 0 Then
            If Row = 14 Or Row = 15 _
            Or Row = 23 Or Row = 24 Then
                GoSub PrintRows
            End If
        End If
    Next RowOrder
    'Next Row
    Return

'*******************************************

PrintRows:
    GoSub PrintRowHeaderLoop
    GoSub PrintColLoop ' grdTable4P.Col
    If Row = 3 Then
        GoSub CheckRow3AltoClef
    End If
    Return

'*******************************************

PrintRowHeaderLoop:
    grdTable4P.Col = 0
    grdTable4P.Row = Row
    Select Case Row
        Case 1, 2, ROW4
            Buf$ = Buf$ & VHdr(Row)
        Case 3
            If gbAlto3 Then
                Buf$ = Buf$ & "%" & VHdr(Row)
            Else
                Buf$ = Buf$ & VHdr(Row)
            End If
        Case 0, ROW5
            Buf$ = Buf$ & "%      "
        Case Else
            Buf$ = Buf$ & "w:     "
       ' See NOT USED #1
    End Select
    ' See NOT USED #2
    'pLineOut = pLineOut + 1 ' not full line yet
    Return

'*******************************************
' Strip out a tilde and use in next loop
' to suppress blanks in between notes
' for beaming eighth or shorter notes
' in ABCplus.
PrintColLoop:
    ABCTabPos = ABCColWidth + 1 ' 8
    ABCPos = ABCTabPos
    For Col = 1 To grdTable4P.Cols - 1
        If Col = 10 Or Col = 11 Then
            If Row = 4 Then
                I = I
            End If
        End If
        grdTable4P.Col = Col
        A$ = Trim$(grdTable4P.Text)
        If A$ = "z" Or A$ = "z" And Row = 4 Then
            I = I
        End If
        ' reinit col pos to beg of next row
        If Row >= 1 And Row <= ROW4 Then
            GoSub FormatColRow1to4
        ElseIf Row > ROW5 Then ' Row 5 is TM:
            GoSub FormatColRowOver5
        End If
        If Tilde$ <> "~" Then
            If ABCPos > ABCColWidth + 1 Then
            ' add a leading space
                Buf$ = Buf$ + " "
                ABCPos = ABCPos + 1
            End If
            If ABCPos < ABCTabPos Then
                Buf$ = Buf$ & Space(ABCTabPos - _
                    ABCPos)
                ABCPos = ABCTabPos
            End If
        End If
        If Row < 6 Then
            I = InStr(A$, "~")
            If I > 0 Then
                Tilde$ = "~"
                If I = Len(A$) Then
                    A$ = Left$(A$, Len(A$) - 1)
                Else
                    A$ = Left$(A$, I - 1) & Mid$(A$, I + 1)
                End If
            Else
                Tilde$ = ""
            End If
        End If
        Buf$ = Buf$ & A$
        ABCPos = ABCPos + Len(A$)
        'If Row = 0 Then
            grdTable4P.Row = 1
            If Left$(grdTable4P.Text, 1) = "|" Then
                'CurrColWidth = ABCCOLWIDTH
                ABCTabPos = ABCPos
            Else
                ABCTabPos = ABCTabPos + ABCColWidth
            End If
            grdTable4P.Row = Row
        'ElseIf A$ = "|" Then
        '    ABCTabPos = ABCPos ' one space past bar
        'Else
        '    ABCTabPos = ABCTabPos + ABCCOLWIDTH
        'End If
    Next Col
    GoSub CheckBufLimits
    Print #pFileNo, Buf$
    pLineOut = pLineOut + 1
    Return
       
'*******************************************

' Reduce a string of blanks to a single blank
' if the Buf$ is too long for abcm2ps.
CheckBufLimits:
       If Not gbBufCompress Then
              If Len(Buf$) > 4095 Then
                     gbBufCompress = True
                     Buf$ = fcnRepDupe(Buf$, " ")
              End If
       Else
              Buf$ = fcnRepDupe(Buf$, " ")
       End If
       Return
       
'*******************************************

' Begin NOT USED
        If Col > 1 Then
            Print #pFileNo, Spc(nSpaces);
            'Debug.Print A$; "/";
        End If
       'If Len(A$) <= 6 Then
            Print #pFileNo, A$;
            nSpaces = 7 - Len(A$)
       'Else
       '    Print #pFileNo, A$; " ";
       'End If
'NextCol:
' End NOT USED
'*******************************************

' A Minus in rows 17-20 means that a NHT is
' in that col. E.g. E,-1 means that the last
' good 4 part chord is back one col.

' An N in rows 17-20 means that the corresponding
' row 1-4 iS A NHT, e.g. E,4,N
FormatColRow1to4:
    AbcLeng$ = ""
    If A$ = "" Then
        GoSub CheckMinusThisCol
        If Not bMinus Then
            'A$ = UCase$("z")
            A$ = "z"
        End If
    End If
    GoSub BarNotes
    GoSub SlurNotes
    GoSub CheckRow4 ' 1sep08/25sep08
    GoSub CheckNHT
    GoSub ConvNote
        'GoSub NoteLength
    A$ = BegSlur$ & NHTPref$ & A$ _
        & AbcLeng$ & Bass4Tie$ & EndSlur$
FormatColRow1to4X:
    Return

'*******************************************

CheckRow4:
    If Row = ROW4 Then
        If A$ <> "|" _
        And A$ <> "||" _
        And A$ <> "z" Then
            ' call this sub after SlurNotes
            ' and before GoSub ConvNote:
            Call WriteAbcNotesBass4(Row, Col, _
                A$, EndSlur$, Bass4Tie$, NewA$)
            A$ = NewA$ ' tied Bass4 note
        End If
    End If
    Return

'*******************************************

CheckMinusThisCol:
    SaveRow = Row
    grdTable4P.Row = SaveRow + 16 ' rows 17-20
    MinusSign = InStr(grdTable4P.Text, "-")
    If MinusSign > 0 Then
        bMinus = True
        MinusPos = _
            Val(Mid$(grdTable4P.Text, MinusSign))
    Else
        bMinus = False
    End If
    grdTable4P.Row = SaveRow
    Return

'*******************************************

' Handle Bar notes
BarNotes:
    If A$ = "|" Then
        Return
    ElseIf A$ = "||" Then
        Return
    ElseIf Row = 1 Then ' drop measure nn from "|: nn"
        If InStr(A$, "||") > 0 Then
            A$ = "||" ' Left$(A$, 2)
            Return
        ElseIf InStr(A$, "|") > 0 Then
            A$ = "|" ' Left$(A$, 1)
            Return
        End If
    End If
    Return

'*******************************************

' Handle  an ABCPlus language Slur "(" or ")"
' or Tie "-".
' A Slur "(" before a note indicates the beginning
' of a musical phrase for that Voice.
' A Slur ")" after a note indicates the end.
' A Tie is a note followed by "-" where the
' next note for this Voice has the same pitch.
' Strip them off $A
' so that A$ can be handled correctly.
' Add them back later in gosub FormatColRow1to4.

' Gosub CheckBassRowMinus may also set
'     BegSlur$ and EndSlur$
' after this gosub.

'Bugs and Limitations of abcMIDI:
'http://abc.sourceforge.net/abcMIDI/original/README.txt
' No field is inherited from above the X: field of
' the tune.

' Where an accidental is applied to a tied note that
' extends across a barline, abc2midi requires that
' the note beyond the barline must be explicitly
' given an accidental e.g.

'  ^F- | F     - will be reported as an error.
'  ^F- | ^F    - will produce a tied ^F note.

'  It is common to see no accidental shown when
' this occurs in published printed music.

SlurNotes:
    BegSlur$ = ""
    EndSlur$ = ""
    If Left$(A$, 1) = "(" Then
        BegSlur$ = "("
        A$ = Mid$(A$, 2)
    End If
    If Right$(A$, 1) = "-)" Then
        EndSlur$ = "-)"
        A$ = Left$(A$, Len(A$) - 2)
    ElseIf Right$(A$, 1) = ")" Then
        EndSlur$ = ")"
        A$ = Left$(A$, Len(A$) - 1)
    ElseIf Right$(A$, 1) = "-" Then
        EndSlur$ = "-"
        A$ = Left$(A$, Len(A$) - 1)
    End If
    Return

'*******************************************

' An N in rows 17-20 means that the corresponding
' row 1-4 iS A NHT, e.g. E,4,N

'To add a paren before and after a note head
' in ABCPlus, to indicate a NHT, use:
'   123456789012
'   "<(  "">)"^F   if an accidental as in F#3.
'   "<("">)"F      if no accidental.

CheckNHT:
    grdTable4P.Row = Row + ROW16
    'If gNote(gNoteX).Voice(Row).bNHT Then
    If Right$(grdTable4P.Text, 1) = "N" Then
        I = I ' testing
        'B$ = Mid$(A$, 2, 1) ' e.g. # in F#3
        'If InStr("#b=", B$) Then
        If Len(A$) > 2 Then '24jul08
            NHTPref$ = """<(  """">)"""
        Else
            NHTPref$ = """<("""">)"""
        End If
    Else
        NHTPref$ = ""
    End If
    'NHTPref$ = ""
    grdTable4P.Row = Row ' reset
    Return

'*******************************************

' Convert my note format to ABCPlus music
' notation
ConvNote:
    A$ = Trim$(A$)
    If A$ = "" Then
        Return
    ElseIf A$ = "|" Then
        Return
    ElseIf A$ = "||" Then
        Return
    ElseIf A$ = "z" Or A$ = "z" Then
        GoSub NoteLength
        Return
    End If

    Err = ""
    B$ = "" ' separate from A$ for debugging
    Call Note2Abc(Row, A$, gbAlto3, B$, Err)
    'call Note2Abc(pVoice As Integer, _
    '            p4Part As String, _
    '            pbAlto3 As Boolean, _
    '            pAbc As String, _
    '            pErr As String)
    If Err = "" Then
        A$ = B$
        GoSub NoteLength
    Else
        GoSub Err2 ' try to keep on
        I = I ' testing
    End If
    Return

'*******************************************

NoteLength:
    I = InStr(txtLength.Text, "/") 'new format "L:1/4"
    If I > 0 Then
        J = InStr(txtLength.Text, "@")
        If J > 0 Then
            MeterLeng = Val(Mid$(txtLength.Text, _
                        I + 1, J - 1)) ' 4
        Else
            MeterLeng = Val(Mid$(txtLength.Text, I + 1)) ' 4
        End If
    Else ' old format "L:4"
        MeterLeng = Val(txtLength.Text) ' 4
    End If
    GoSub GetNoteTime
    Err = ""
    Call Time2Abc(MeterLeng, NoteLeng$, _
        AbcLeng$, Err)
    If Err <> "" Then
        GoSub Err2 ' try to keep on
        I = I ' testing
    End If
    Return

'*******************************************

' Get the length char (W H Q E S T) in the time field,
' with any > or < chars following it.
' TODO what was I going to use ":" in the
' time field for?

GetNoteTime:
    If Row >= ROW5 Then ' at FormatColRowOver5
        NoteLeng$ = grdTable4P.Text
    Else ' Row <= Row4
        ' Since rest z doesn't go to
        ' sub WriteAbcNotesBass4, use Row+16
        
        If A$ = "z" Then
            grdTable4P.Row = Row + 16 ' rows 17-20
        ElseIf Row <> ROW4 Then ' T4,pos
            grdTable4P.Row = Row + 16 ' rows 17-20
        Else ' 1sep08 (if testing with Bass4 blank)
            grdTable4P.Row = ROW21 ' Bass4
            If Trim$(grdTable4P.Text) = "" Then
                grdTable4P.Row = Row + 16 ' rows 17-20
            End If
        End If
        
        ' Using Col and Row that was set up for:
        I = InStr(grdTable4P.Text, ",") ' Q,-1
        If I > 1 Then
            NoteLeng$ = Left$(grdTable4P.Text, I - 1)
        Else
            'Stop
            I = I ' testing
        End If
        grdTable4P.Row = Row ' reset
    End If
    Return

'*******************************************
' TODO 12/2/6 not used
        grdTable4P.Row = Row + 16 ' rows 17-20
        If A$ = "z" Then
            '
        ElseIf Row = ROW4 Then ' T4,pos
            If gNumLyricLines > 0 Then
                grdTable4P.Row = 21 ' Bass4
            End If
        End If

'*******************************************
' END OF FormatColRow1to4:
'*******************************************

FormatColRowOver5:
    If A$ = "|" Then
        If Row = 14 Or Row = 15 _
        Or Row = 23 Or Row = 24 Then
            '
        Else
            A$ = ""
            Return
        End If
    End If
      
    If A$ = "" Then
        ' do not clobber lyric lines 1sep08
        If Row = 14 Or Row = 15 _
        Or Row = 23 Or Row = 24 Then
            '
        Else
            A$ = "*"
        End If
    End If
    GoSub GetNumRests
    If NumRests >= 4 Then
        A$ = ""
        'GoTo NextCol
    End If
    GoSub GetNoteTime
    If NoteLeng$ = "|" _
    Or NoteLeng$ = "||" Then
        If Col > 1 Then
       '     Print #pFileNo, Spc(nSpaces);
        End If
       'GoTo NextCol
    Else
        GoSub ConvText
    End If
    Return

'*******************************************

' If there are all four rests at this note
' position, then skip any printing to ABC file
' for the rest of this column
GetNumRests:
    SaveRow = Row
    NumRests = 0
    For J = 1 To 4
        grdTable4P.Row = J
        Rest1Char$ = Left$(grdTable4P.Text, 1)
        'If UCase$(Rest1Char$) = "z" Then
        If Rest1Char$ = "z" Then
            NumRests = NumRests + 1
        End If
    Next J
    grdTable4P.Row = SaveRow
    Return

'*******************************************

' ABCPlus lyric lines "w:  "
' Suppress the bar "|" going to ABCPlus because
' if the first bar is the first on the line, it
' throws off the alignment of the lyrics.
' Bars are not displayed in the lyrics w: lines
' anyway.30aug08

ConvText:
    If A$ = "" Then
      Return
    End If
    If IsNumeric(Left$(A$, 1)) Then
        A$ = "~" & A$
        ' TODO align ~ one space to Left$
        'If nSpaces > 1 Then
        '    nSpaces = nSpaces - 1
        'End If
    End If
    A$ = fcnRepChar(A$, " ", "~") ' call
    'If Row = 6 Or Row = 12 Then
        GoSub OuterInnerTemp
    'End If
    
    'A$ = " " & A$
    If Len(A$) > 6 Then
        If Left$(A$, 1) = "~" Then
            A$ = Mid$(A$, 2)
        End If
    End If
    If Not bWroteKey Then
        If Row = ROW6 Then
            If A$ <> "" Then
                GoSub GetKey
                bWroteKey = True
            End If
        End If
    End If
    Return
    
'*******************************************

    ' In Outer/Inr line replace the ~~~~~ with ~~
OuterInnerTemp:
    A$ = fcnReplace(A$, "~~~~~", "~")
    A$ = fcnReplace(A$, "~~~~", "~")
    A$ = fcnReplace(A$, "~~~", "~")
    ' \ is an escape char in ABC
    'A$ = fcnRepChar(A$, "\", "L")
    'A$ = fcnReplace(A$, "//", "/")
    ' in ABC, w: treats - and _ specially
    'A$ = fcnRepChar(A$, "-", "=")
    'A$ = fcnRepChar(A$, "_", "=")
    Return

'*******************************************

' Get the key from the screen to display to the
' left of the Rom Num of the first note in the
' piece.  Add "m" if a minor key.  E.g. Bbm:~I
GetKey:
    grdKey.Col = 0
    grdKey.Row = 0
    B$ = Trim$(grdKey.Text)
    grdKey.Row = 1
    If grdKey.Text = "MINOR" Then
        B$ = B$ & "m"
    End If
    A$ = B$ & ":~" & A$
    Return
    
'*******************************************
' END OF FormatColRowOver5:
'*******************************************

' In gosub ConvNote, see
'    call Note2Abc(A$, Row, gbAlto3, B$, Err)
CheckRow3AltoClef:
    If gbAlto3 Then
        gbAlto3 = False
    Else ' If Not gbAlto3 Then
        ' Redo row 3 as a comment line
        ' to print out a tenor
        ' line for the alto clef, but
        ' do not adjust the notes up
        ' by two octaves in sub Note2Abc.
        ' Then in JEDAbc, I can manually
        ' uncomment out the Alto3 line
        ' and print it in the Alto clef.
        gbAlto3 = True
        Row = Row - 1 ' Row 2
    End If
    Return

'*******************************************
' END OF PrintNoteLoop:
'*******************************************

Err2:
    bErr = True
    gInputErrs = gInputErrs + 1
    GoSub GetNoteNo
    E$ = "WriteAbcNotes: " & _
        "invalid note=" & A$ & _
        " at note no=" & B$ & _
        " in SongNo=" & pSongno & Err
    Call DisplayText(E$)
    Debug.Print E$
    Return

'*******************************************

GetNoteNo:
    SaveRow = Row
    grdTable4P.Row = 1 ' note nos
    B$ = grdTable4P.Text
    grdTable4P.Row = SaveRow
    Return

'*******************************************

' BEGIN OF NOT USED #1
           'Case 6
           '    Print #pFileNo, "w:RNum~";
           'Case 7
           '    Print #pFileNo, "w:FigB~";
           'Case 8
           '    Print #pFileNo, "w:Vari~";
           'Case 9
           '    Print #pFileNo, "w:Cand~";
           'Case 10
           '    Print #pFileNo, "w:Crd~";
           'Case 11
           '    Print #pFileNo, "w:Ntvl~";
           'Case 12
           '    Print #pFileNo, "w:Lyrl~";
           'Case 13
           '    Print #pFileNo, "w:Lyr2~";
           'Case 14
           '    Print #pFileNo, "w:Lyr3~";
           'Case 15
           '    Print #pFileNo, "w:Lyr4~";
' END OF NOT USED #1

'*******************************************

' BEGIN OF NOT USED #2
       'If Row >= 1 And Row < 4 Then
       '    Print #pFileNo, VHdr(Row);
       'ElseIf Row = ROW4 Then
       '    If gbAlto3 Then
       '        Print #pFileNo, VHdr(Row);
       '    Else
       '        Print #pFileNo, VHdr(Row);
       '    End If
       'ElseIf Row > 5 Then
       '    Print #pFileNo, "w:    ";
       'Else ' Row=5
       '    Print #pFileNo, "%      ";
       'End If
' END OF NOT USED #2

End Sub


Private Sub WriteAbcNotes(pSongno As Integer, _
                        pFileNo As Integer, _
                        pLineOut As Integer)
    ' Called by WriteAbc

    ' Calls sub WriteAbcNotesBass4
    ' Calls sub DisplayText
    ' Calls sub Note2Abc
    ' Calls sub Time2Abc
    ' Calls function fcnRepChar
    ' Calls function fcnReplace

    ' Globals used: gPrefix, gDirSongs, gFile
    ' Globals set:
    
    ' 12/2/6.
    ' Notice that I cannot bypass calling
    ' sub WriteAbcNotesBass4
    ' even if there are no lyric lines.
    ' The w: lines of Rom Num and Figured Bass.
    ' lines 6 and 7, depend on the bass notes
    ' having the most notes in the measure.
    ' It is a constraint of ABCPlus or abcm2ps.
    
    ' Possibly I can insert invisible x or y
    ' notes to map the w: fields to. (NOT !)
    
    ' 2/16/8.
    ' Build the output line, Buf$ = Buf$ +,
    ' instead of Print #pFileNo, xxx;
    ' so that I can scan for the "beaming" operator ~,
    ' and remove the spaces between the notes
    ' that are to be beamed in ABC.

    ' After Option
    '   OP: MISC WRITE ROW4 % default is BASSROW4
    ' Sub WriteABCNotes will
    ' write out ROW4 instead of BASSROW4, and will
    ' not write out the harmonic analysis.
    ' This is to prepare a clean score for performers.

    Dim I As Integer
    Dim J As Integer
    Dim nSpaces As Integer
    Dim Row As Integer
    Dim Col As Integer
    Dim Col1 As Integer
    Dim Col2 As Integer
    Dim SaveRow As Integer
    Dim SaveCol As Integer
    Dim VHdr As Variant ' SOPRANO To BASS
    Dim A$
    Dim B$
    Dim MeterLeng As Integer
    Dim NoteLeng$
    Dim AbcLeng$
    Dim Err As String
    Dim bErr As Boolean
    Dim E$
    Dim BegSlur$
    Dim EndSlur$
    Dim Bass4Tie$
    Dim Rest1Char$
    Dim NumRests As Integer
    Dim Minus As Boolean
    Dim MinusSign As Integer
    Dim MinusPos As Integer
    Dim NewA$
    Dim NHTPref$
    Dim Buf$
    Dim Tilde$
    ' My Convention: use Const's instead of
    ' constants so that it is easy to
    ' update the row numbers.
    Const ROW0 As Integer = 0
    Const ROW1 As Integer = 1
    Const ROW2 As Integer = 2
    Const ROW3 As Integer = 3
    Const ROW4 As Integer = 4
    Const ROW5 As Integer = 5 ' length
    Const ROW6 As Integer = 6 ' Roman numerals
    Const ROW7 As Integer = 7 ' Figured bass
    Const ROW14 As Integer = 14 ' lyric line 1
    Const ROW15 As Integer = 15 ' lyric line 2
    Const ROW16 As Integer = 16
    Const ROW17 As Integer = 17 ' T1 line
    Const ROW20 As Integer = 20 ' T4 line
    Const ROW21 As Integer = 21 ' Bass4 T4 line
    Const ROW23 As Integer = 23 ' lyric line 3
    Const ROW24 As Integer = 24 ' lyric line 4
    Const ABCColWidth As Integer = 7

    'Dim ABCPos As Integer
    'Dim ABCTabPos As Integer
    'Dim bWroteKey As Boolean

    'Dim CurrColWidth As Integer
    Dim ABCPos As Integer
    Dim ABCTabPos As Integer
    Dim bWroteKey As Boolean
    Dim RowOrder As Integer
    Dim NumPrintRows As Integer
    Dim RowPrintOrder As Variant
    Dim gbBufCompress As Boolean
    Dim SaveEndSlur$
    Dim Note As Integer
    Dim RowDecrement As Integer
    Dim nBars As Integer
    Dim bEndofCols As Boolean
    Dim Quoted$
    Dim PreNote$
    Dim PostNote$
    Dim MeasNo As Integer
    Dim NoteNo As Integer
    Dim BarNoteNo As Integer

'*******************************************
'*******************************************

Main:
    GoSub Init
    GoSub InitRows
    'If bErr Then
    '    Exit Sub
    'End If
    GoSub PrintNBarsPerLine '6dec08
    'GoSub PrintNoteLoop ' grdTable4P.Row,
                        ' grdTable4P.Col
        'GoSub PrintRowHeaderLoop
                        ' grdTable4P.Row
        'GoSub PrintColLoop ' grdTable4P.Col
            'GoSub FormatColRow1to4
                'GoSub CheckMinusThisCol
                'GoSub BarNotesRow1To4
                'GoSub SlurNotes
                'Call WriteAbcNotesBass4
                'GoSub CheckNHT
                'GoSub ConvNote
                    'Call Note2Abc
                    'GoSub NoteLength
                        'GoSub GetNoteTime
                        'Call Time2Abc
                        'GoSub Err2
                    'GoSub Err2
            'GoSub FormatColRowOver5
                'GoSub GetNumRests
                'GoSub GetNoteTime
                'GoSub ConvText
                'fcnRepChar
        'GoSub CheckRow3AltoClef
    Exit Sub

'*******************************************
' END OF Main:
'*******************************************

Init:
    VHdr = Array("", "[V: S] ", "[V: A] ", _
                    "[V: T] ", "[V: B] ")
    ' If gbAlto3 then output data for the Alto
    ' voice for printing by abcm2ps in an
    ' individual part Alto clef.
    gbAlto3 = False ' 12oct08
    bWroteKey = False
    gbBufCompress = False ' 15dec08
    MeasNo = 0
    NoteNo = 0
    BarNoteNo = 0
    Return

'*******************************************

InitRows:
    'RowPrintOrder = Array(1, 2, 3, 4, 13, 14, 22, 23, _
        6, 7, 8, 9, 12)
        
    NumPrintRows = 14
    If gbOneLyricLinePerVoice Then
        ' one w: after each row 1-4
        RowPrintOrder = Array(0, 1, 14, 2, 15, _
            3, 23, 4, 24, _
            6, 7, 8, 9, 12)
    Else ' w: after rows 1-4
        RowPrintOrder = Array(0, 1, 2, 3, 4, _
            14, 15, 23, 24, _
            6, 7, 8, 9, 12)
    End If

    If gbShowInputNotesOnly Then
        NumPrintRows = 9
    End If
    If gbWriteRow4 Then
        NumPrintRows = 9
    End If
    'CurrColWidth = ABCColWidth
    Return

'*******************************************
' END OF Init:
'*******************************************

PrintNBarsPerLine:
    bEndofCols = False
    Col2 = 0
    GoSub GetCol2Limit
    Do
        GoSub PrintNoteLoop
        If bEndofCols Then
            Exit Do
        Else
            GoSub GetCol2Limit
        End If
    Loop
    Return

'*******************************************

' Get Col1 and Col2 for use at PrintColLoop:
' Col2 = Col1 + gBarsPerLine - 1

GetCol2Limit:
    nBars = 0
    Col1 = Col2 + 1
    grdTable4P.Row = ROW5 ' find bar in TM row
    For Col = Col1 To grdTable4P.Cols - 1
        grdTable4P.Col = Col
        A$ = Trim$(grdTable4P.Text)
        If A$ = "|" Or A$ = "||" Then
            nBars = nBars + 1
            If nBars >= gBarsPerLine Then
                Col2 = Col
                Exit For
            End If
        End If
    Next Col
    If Col >= grdTable4P.Cols - 1 Then 'For loop done
        Col2 = grdTable4P.Cols - 1
        bEndofCols = True
    End If
    Return
    
'*******************************************
    
PrintNoteLoop:
    NumRests = 0
    RowDecrement = 0
    ' Do not print the last row (16) of Chord#'s
    ' and the last four T1: to T4: rows (17-20).
    ' and the last Bass4: row (21).
    'For Row = 0 To grdTable4P.Rows - 1
    For RowOrder = 0 To NumPrintRows - 1
        Row = RowPrintOrder(RowOrder)
        Buf$ = ""
        If (Row >= 0 And Row <= 12) Then
            GoSub PrintRows
        ElseIf gNumLyricLines > 0 Then
            If Row = 14 Or Row = 15 _
            Or Row = 23 Or Row = 24 Then
                GoSub PrintRows
            End If
        End If
    Next RowOrder
    'Next Row
    Buf$ = "%" ' spacer between groups of lines
    Print #pFileNo, Buf$
    pLineOut = pLineOut + 1
    Return

'*******************************************

PrintRows:
    GoSub PrintRowHeaderLoop
    GoSub PrintColLoop ' grdTable4P.Col
    Return

    ' NOT USED 6dec08
    ' no need for a separate line; just
    ' change the tenor clef to clef=alto
    ' If Row=3 and gbAlto3 then redo Row3
    If Row = 3 Then
        GoSub CheckRow3AltoClef
        RowOrder = RowOrder - RowDecrement
    End If

'*******************************************

PrintRowHeaderLoop:
    grdTable4P.Col = 0
    grdTable4P.Row = Row
    Select Case Row
        Case 1, 2, ROW4
            Buf$ = VHdr(Row)
        Case 3
            If gbAlto3 Then
                Buf$ = "%" & VHdr(Row)
            Else
                Buf$ = VHdr(Row)
            End If
        'Case 0, ROW5
        Case 0, ROW0 ' 8jan09
            Buf$ = ""
        Case Else
            Buf$ = "w:     "
       ' See NOT USED #1
    End Select
    ' See NOT USED #2
    'pLineOut = pLineOut + 1 ' not full line yet
    Return

'*******************************************
' Strip out a tilde and use in next loop
' to suppress blanks in between notes
' for beaming eighth or shorter notes
' in ABCplus.
PrintColLoop:
    ABCTabPos = ABCColWidth + 1 ' 8
    ABCPos = ABCTabPos
    'For Col = 1 To grdTable4P.Cols - 1
    For Col = Col1 To Col2 ' 6dec08
        grdTable4P.Col = Col
        A$ = Trim$(grdTable4P.Text)
        GoSub Test1
        'GoSub Test2
        ' reinit col pos to beg of next row
        If Row = ROW0 Then
            GoSub FormatColRow0
        ElseIf Row >= 1 And Row <= ROW4 Then
            GoSub FormatColRow1to4
        ElseIf Row > ROW5 Then ' Row 5 is TM:
            GoSub FormatColRowOver5
        End If
        If Tilde$ <> "~" Then
            If ABCPos > ABCColWidth + 1 Then
            ' add a leading space
                Buf$ = Buf$ + " "
                ABCPos = ABCPos + 1
            End If
            If ABCPos < ABCTabPos Then
                Buf$ = Buf$ & Space(ABCTabPos - _
                    ABCPos)
                ABCPos = ABCTabPos
            End If
        Else
            Tilde$ = "" ' 9jan09
        End If
        If Row > 0 And Row < 6 Then
            I = InStr(A$, "~")
            If I > 0 Then
                Tilde$ = "~"
                A$ = fcnRepChar(A$, "~", "")
                'If I = Len(A$) Then
                '    A$ = Left$(A$, Len(A$) - 1)
                'Else
                '    A$ = Left$(A$, I - 1) & Mid$(A$, I + 1)
                'End If
            Else
                Tilde$ = ""
            End If
        End If
        If Row >= 1 And Row <= 4 Then
            If Right$(Buf$, 1) <> " " Then
                Buf$ = Buf$ & " "
                ABCPos = ABCPos + 1
            End If
        End If
        Buf$ = Buf$ & A$
        ABCPos = ABCPos + Len(A$)
        GoSub CheckBarSpaces
    Next Col
    
    GoSub CheckBufLimits
    If Left$(Buf$, 2) <> "w:" Then
        Print #pFileNo, Buf$
        pLineOut = pLineOut + 1
    ElseIf Len(Trim$(Buf$)) > 2 Then
        Print #pFileNo, Buf$
        pLineOut = pLineOut + 1
    End If
    Return
    
'*******************************************

' Recalculate and print a line before
' the TM: line, containing measure numbers
' and note numbers.

FormatColRow0:
    grdTable4P.Row = ROW5 ' TM: line
    B$ = Trim$(grdTable4P.Text)
    If Col = Col1 Then
        MeasNo = MeasNo + 1
        Buf$ = "%m:" & Format(MeasNo)
        If Len(Buf$) < ABCColWidth Then
            Buf$ = Buf$ & _
                Space(ABCColWidth - Len(Buf$))
        End If
        BarNoteNo = -1 ' <> NoteNo
    End If
    If Col < Col2 Then
        If B$ = "|" Or B$ = "||" Then
            MeasNo = MeasNo + 1
            A$ = B$
            NoteNo = NoteNo + 1
            BarNoteNo = NoteNo
        Else
            If NoteNo = BarNoteNo Then
                BarNoteNo = 0
                NoteNo = NoteNo + 1
                A$ = "m:" & Format(MeasNo)
            Else
                NoteNo = NoteNo + 1
                A$ = Format(NoteNo)
            End If
        End If
    Else ' at end of line
        NoteNo = NoteNo + 1
        A$ = "|"
    End If
    Row = ROW0
    Return
        
'*******************************************

CheckBarSpaces:
       grdTable4P.Row = 1
       If Left$(grdTable4P.Text, 1) = "|" Then
           ABCTabPos = ABCPos ' one space past bar
       Else
           ABCTabPos = ABCTabPos + ABCColWidth
       End If
       grdTable4P.Row = Row
       Return
       
' NOT USED 1oct08
        If Row = 0 Then
            grdTable4P.Row = 1
            If Left$(grdTable4P.Text, 1) = "|" Then
                ABCTabPos = ABCPos
            Else
                ABCTabPos = ABCTabPos + ABCColWidth
            End If
            grdTable4P.Row = Row
        ElseIf A$ = "|" Then
            ABCTabPos = ABCPos ' one space past bar
        Else
            ABCTabPos = ABCTabPos + ABCColWidth
        End If
        Return
       
'*******************************************

Test1B:
    If Col = 31 Or Col = 32 Or Col = 33 Then
        If Row = 2 Then
            Debug.Print "WriteAbcNotes:E3 "; _
            "row col A$="; Row; Col; A$
           I = I
        End If
    End If
    Return
       
'*******************************************

Test1:
    If Col = 15 And Row = 1 Then
        I = I
    End If
    Return
       
'*******************************************

Test2:
    If A$ = "z" Or A$ = "z" And Row = 4 Then
        I = I
    End If
    Return
       
'*******************************************

' Reduce a string of blanks to a single blank
' if the Buf$ is too long for abcm2ps.
CheckBufLimits:
    If Not gbBufCompress Then
        If Len(Buf$) > 4095 Then
            gbBufCompress = True
            Buf$ = fcnRepDupe(Buf$, " ")
        End If
    Else
        Buf$ = fcnRepDupe(Buf$, " ")
    End If
    Return
       
'*******************************************
' Begin NOT USED
        If Col > 1 Then
            Print #pFileNo, Spc(nSpaces);
            'Debug.Print A$; "/";
        End If
       'If Len(A$) <= 6 Then
            Print #pFileNo, A$;
            nSpaces = 7 - Len(A$)
       'Else
       '    Print #pFileNo, A$; " ";
       'End If
'NextCol:
' End NOT USED
'*******************************************

' A minus in rows 17-20 means that a NHT is
' in that col. E.g. E,-1 means that the last
' good 4 part chord is back one col.

' An N in rows 17-20 means that the corresponding
' row 1-4 iS A NHT, e.g. E,4,N
FormatColRow1to4:
    GoSub Test3
    AbcLeng$ = ""
    If A$ = "" Then
        GoSub CheckMinusThisCol
        If Not Minus Then
            'A$ = UCase$("z")
            A$ = "z"
        End If
    End If
    GoSub BarNotesRow1To4
    
    BegSlur$ = ""
    EndSlur$ = ""
    GoSub SlurNotes
    If Not gbWriteRow4 Then
        GoSub ConvertBassRow
    End If
    GoSub CheckNHT
    GoSub ConvNote
        'GoSub NoteLength
    GoSub CheckChangeToRest
    
    ' Must avoid Bass4Tie$="-~" and
    ' EndSlur$=")" to avoid copying
    ' "-~)" to the end of Buf$. 1oct08
    BegSlur$ = "" ' 6jan09
    EndSlur$ = "" ' 6jan09
    If Bass4Tie$ = "" Then
       SaveEndSlur$ = ""
       A$ = BegSlur$ & NHTPref$ & A$ _
           & AbcLeng$ & Bass4Tie$ & EndSlur$
    Else
       SaveEndSlur$ = EndSlur$
       A$ = BegSlur$ & NHTPref$ & A$ _
           & AbcLeng$ & Bass4Tie$
    End If
    GoSub BeforeAndAfterControls
    If Quoted$ <> "" _
    Or PreNote$ <> "" _
    Or PostNote$ <> "" Then ' 6jan09
        A$ = fcnPack(A$) ' call
        A$ = Quoted$ & PreNote$ & A$ & PostNote$
        Quoted$ = ""
        PreNote$ = ""
        PostNote$ = ""
    End If
    Return
    
'*******************************************

Test3:
    If Col = 183 And Row = 2 Then
        I = I
    End If
    Return
    
'*******************************************

BeforeAndAfterControls:
    Note = Col - gSong(gSongCurr).FirstNote + 1
    Quoted$ = gNote(Note).Voice(Row).Quoted
    PreNote$ = gNote(Note).Voice(Row).PreNote
    PostNote$ = gNote(Note).Voice(Row).PostNote
    Return
    
'*******************************************

CheckChangeToRest:
    Note = Col - gSong(gSongCurr).FirstNote + 1
    If gNote(Note).Voice(Row).xWriteXZ = 128 Then
        A$ = "z"
    ElseIf gNote(Note).Voice(Row).xWriteXZ = 1 Then
        A$ = "x"
    End If
    Return
    
'*******************************************

ConvertBassRow:
    If Row = ROW4 Then
        If A$ <> "|" _
        And A$ <> "||" _
        And A$ <> "z" Then
            ' Call this sub after SlurNotes
            ' and before GoSub ConvNote:
            Call WriteAbcNotesBass4(Row, Col, A$, _
                BegSlur$, EndSlur$, Bass4Tie$, NewA$)
            A$ = NewA$ ' tied Bass4 note
            'If EndSlur$ = "" Then
            '    ' e.g. in case A$="B2)"
            '    GoSub SlurNotes ' 1oct08
            'End If
        End If
    End If
    Return
    
'*******************************************

CheckMinusThisCol:
    SaveRow = Row
    grdTable4P.Row = SaveRow + 16 ' rows 17-20
    MinusSign = InStr(grdTable4P.Text, "-")
    If MinusSign > 0 Then
        Minus = True
        MinusPos = _
            Val(Mid$(grdTable4P.Text, MinusSign))
    Else
        Minus = False
    End If
    grdTable4P.Row = SaveRow
    Return

'*******************************************

' Handle Bar notes
BarNotesRow1To4:
    If A$ = "|" Then
        Return
    ElseIf A$ = "||" Then
        Return
    ElseIf Row = 1 Then ' drop measure nn from "|: nn"
        If InStr(A$, "||") > 0 Then
            A$ = "||" ' Left$(A$, 2)
            Return
        ElseIf InStr(A$, "|") > 0 Then
            A$ = "|" ' Left$(A$, 1)
            Return
        End If
    End If
    Return

'*******************************************

' Handle  an ABCPlus language Slur "(" or ")"
' or Tie "-".
' A Slur "(" before a note indicates the beginning
' of a musical phrase for that Voice.
' A Slur ")" after a note indicates the end.
' A Tie is a note followed by "-" where the
' next note for this Voice has the same pitch.
' Strip them off $A
' so that A$ can be handled correctly.
' Add them back later in gosub FormatColRow1to4.

' Gosub CheckBassRowMinus may also set
'     BegSlur$ and EndSlur$
' after this gosub.

'Bugs and Limitations of abcMIDI:
'http://abc.sourceforge.net/abcMIDI/original/README.txt
' No field is inherited from above the X: field of
' the tune.

' Where an accidental is applied to a tied note that
' extends across a barline, abc2midi requires that
' the note beyond the barline must be explicitly
' given an accidental e.g.

'  ^F- | F     - will be reported as an error.
'  ^F- | ^F    - will produce a tied ^F note.

'  It is common to see no accidental shown when
' this occurs in published printed music.

SlurNotes:
    GoSub StripBegSlurs
    GoSub StripEndSlursAndTie
    Return

'*******************************************

' Strip off slurs at beginning of A$
StripBegSlurs:
    J = 0
    For I = 1 To Len(A$)
        If Mid$(A$, I, 1) <> "(" Then
            J = I
            Exit For
        End If
    Next I
    If J > 1 Then
        BegSlur$ = Left$(A$, J - 1)
        A$ = Mid$(A$, J)
    Else
        BegSlur$ = ""
    End If
    Return

'*******************************************

' Strip off the -'s and )'s found at the end
' of A$, starting with the first - or ) found.
StripEndSlursAndTie:
    ' Find the first - or )
    I = InStr(A$, "-")
    J = InStr(A$, ")")
    If I = 0 Then
        I = J
    ElseIf J > 0 And J < I Then
        I = J
    End If
    
    If I > 0 Then
        EndSlur$ = Mid$(A$, I)
        A$ = Left$(A$, I - 1)
    Else
        EndSlur$ = ""
    End If
    Return
    
'*******************************************

' An N in rows 17-20 means that the corresponding
' row 1-4 iS A NHT, e.g. E,4,N

'To add a paren before and after a note head
' in ABCPlus, to indicate a NHT, use:
'   123456789012
'   "<(  "">)"^F   if an accidental as in F#3.
'   "<("">)"F      if no accidental.

CheckNHT:
    grdTable4P.Row = Row + ROW16
    'If gNote(gNoteX).Voice(Row).bNHT Then
    If Right$(grdTable4P.Text, 1) = "N" Then
        I = I ' testing
        'B$ = Mid$(A$, 2, 1) ' e.g. # in F#3
        'If InStr("#b=", B$) Then
        If Len(A$) > 2 Then '24jul08
            NHTPref$ = """<(  """">)"""
        Else
            NHTPref$ = """<("""">)"""
        End If
    Else
        NHTPref$ = ""
    End If
    'NHTPref$ = ""
    grdTable4P.Row = Row ' reset
    Return

'*******************************************

' Convert my note format to ABCPlus music
' notation
ConvNote:
    A$ = Trim$(A$)
    If A$ = "B4)" Then
       I = I
    End If
    If A$ = "" Then
        Return
    ElseIf A$ = "|" Then
        Return
    ElseIf A$ = "||" Then
        Return
    ElseIf A$ = "z" Or A$ = "z" Then
        GoSub NoteLength
        Return
    End If

    Err = ""
    B$ = "" ' separate from A$ for debugging
    Call Note2Abc(Row, A$, gbAlto3, B$, Err)
    'call Note2Abc(pVoice As Integer, _
    '            p4Part As String, _
    '            pbAlto3 As Boolean, _
    '            pAbc As String, _
    '            pErr As String)
    If Err = "" Then
        A$ = B$
        GoSub NoteLength
    Else
        GoSub Err2 ' try to keep on
        I = I ' testing
    End If
    Return

'*******************************************

NoteLength:
    I = InStr(txtLength.Text, "/") 'new format "L:1/4"
    If I > 0 Then
        MeterLeng = Val(Mid$(txtLength.Text, I + 1, 2)) ' 4
    Else ' old format "L:4"
        MeterLeng = Val(txtLength.Text) ' 4
    End If
    GoSub GetNoteTime
    Err = ""
    Call Time2Abc(MeterLeng, NoteLeng$, _
        AbcLeng$, Err)
    If Err <> "" Then
        GoSub Err2 ' try to keep on
        I = I ' testing
    End If
    Return

'*******************************************

' Get the length char (W H Q E S T) in the time field,
' with any > or < chars following it.
' TODO what was I going to use ":" in the
' time field for?

GetNoteTime:
    If Row >= ROW5 Then ' at FormatColRowOver5
        NoteLeng$ = grdTable4P.Text
    Else ' Row <= Row4
        ' Since rest z doesn't go to
        ' sub WriteAbcNotesBass4, use Row+16
        'If A$ = UCase$("z") Then
        If A$ = "z" Then
            grdTable4P.Row = Row + 16 ' rows 17-20
        ElseIf Row <> ROW4 Then ' T4,pos
            grdTable4P.Row = Row + 16 ' rows 17-20
        ElseIf gbWriteRow4 Then
            grdTable4P.Row = Row + 16 ' rows 17-20
        Else
            ' 1sep08 (if testing with Bass4 blank)
            grdTable4P.Row = ROW21 ' Bass4
            If Trim$(grdTable4P.Text) = "" Then
                grdTable4P.Row = Row + 16 ' rows 17-20
            End If
        End If
        
        ' Using Col and Row that was set up for:
        I = InStr(grdTable4P.Text, ",") ' Q,-1
        If I > 1 Then
            NoteLeng$ = Left$(grdTable4P.Text, I - 1)
        Else
            I = I ' testing
            Stop
        End If
        grdTable4P.Row = Row ' reset
    End If
    Return

'*******************************************

' TODO 12/2/6 not used
        grdTable4P.Row = Row + 16 ' rows 17-20
        If A$ = "z" Then
            '
        ElseIf Row = ROW4 Then ' T4,pos
            If gNumLyricLines > 0 Then
                grdTable4P.Row = 21 ' Bass4
            End If
        End If

'*******************************************
' END OF FormatColRow1to4:
'*******************************************

FormatColRowOver5:
    If A$ = "*" Then
       I = I
    End If
    If Row = 7 Then
        If A$ = "" Then
            A$ = "*"
        End If
    End If

    'If A$ = "" Then A$ = "*"
    GoSub GetNumRests
    If NumRests >= 4 Then
        A$ = ""
        'GoTo NextCol
    End If
    If A$ = "|" Or A$ = "||" Then
        If Row = 14 Or Row = 15 _
        Or Row = 23 Or Row = 24 Then
            A$ = fcnRepChar(A$, "|", " ") ' 1oct08
        End If
    End If
    GoSub GetNoteTime
    If NoteLeng$ = "|" _
    Or NoteLeng$ = "||" Then
        If Row = 14 Or Row = 15 _
        Or Row = 23 Or Row = 24 Then
            NoteLeng$ = ""
        End If
        If Col > 1 Then
       '     Print #pFileNo, Spc(nSpaces);
        End If
       'GoTo NextCol
    Else
        GoSub ConvText
    End If
    Return

'*******************************************

' If there are all four rests at this note
' position, then skip any printing to ABC file
' for the rest of this column
GetNumRests:
    SaveRow = Row
    NumRests = 0
    For J = 1 To 4
        grdTable4P.Row = J
        Rest1Char$ = Left$(grdTable4P.Text, 1)
        'If UCase$(Rest1Char$) = "z" Then
        If Rest1Char$ = "z" Then
            NumRests = NumRests + 1
        End If
    Next J
    grdTable4P.Row = SaveRow
    Return

'*******************************************

' ABCPlus lyric lines "w:  "
ConvText:
    If A$ = "" Then
        Return
    End If
    If IsNumeric(Left$(A$, 1)) Then
        A$ = "~" & A$
        ' TODO align ~ one space to Left$
        'If nSpaces > 1 Then
        '    nSpaces = nSpaces - 1
        'End If
    End If
    
    A$ = fcnRepChar(A$, " ", "~") ' Call
    'If Row = 6 Or Row = 12 Then
        GoSub OuterInnerTemp
    'End If
    
    'A$ = " " & A$
    If Len(A$) > 6 Then
        If Left$(A$, 1) = "~" Then
            A$ = Mid$(A$, 2)
        End If
    End If
    If Not bWroteKey Then
        If Row = ROW6 Then
            If A$ <> "" Then
                GoSub GetKey
                bWroteKey = True
            End If
        End If
    End If
    GoSub ConvertSlurParens
    Return
    
'*******************************************

' I adopted the practice to use
' parens are used in Noteworthy Composer LYRIC
' lines in Voice 1 to indicate that the notes
' above are to be slurred.
' E.g. I manually change the word tur,
' which is prolonged in two syllables with
' an underscore,
'   tur_
' into
'   (tur )

' In my program 4Part.vbp, then notes above them
' in Voice 1,
'   e.g. G2 A2
' are slurred as if the following were entered in
' ABC notation:
'   (*G2 A2*)
' Then all the other notes in that chord are
' also slurred. 15dec08

' Here, I remove "(" and I replace ")" with "_".

ConvertSlurParens:
    If gNumLyricLines > 0 Then
        If Row = ROW14 Then
            If Left$(A$, 1) = "(" Then
                A$ = Mid$(A$, 2)
            ElseIf A$ = ")" Then
                A$ = "_"
            End If
        End If
    End If
    Return
        
'*******************************************

    ' In Outer/Inr line replace the ~~~~~ with ~~
OuterInnerTemp:
    'A$ = fcnReplace(A$, "~~~~~", "~")
    'A$ = fcnReplace(A$, "~~~~", "~")
    'A$ = fcnReplace(A$, "~~~", "~")
    A$ = fcnRepDupe(A$, "~") ' shrink to one ~
    'A$ = fcnRepChar(A$, "~", "") ' drop all ~'s
    ' \ is an escape char in ABC
    'A$ = fcnRepChar(A$, "\", "L")
    'A$ = fcnReplace(A$, "//", "/")
    ' in ABC, w: treats - and _ specially
    'A$ = fcnRepChar(A$, "-", "=")
    'A$ = fcnRepChar(A$, "_", "=")
    Return

'*******************************************

' Get the key from the screen to display to the
' left of the Rom Num of the first note in the
' piece.  Add "m" if a minor key.  E.g. Bbm:~I
GetKey:
    grdKey.Col = 0
    grdKey.Row = 0
    B$ = Trim$(grdKey.Text)
    grdKey.Row = 1
    If grdKey.Text = "MINOR" Then
        B$ = B$ & "m"
    End If
    A$ = B$ & ":~" & A$
    Return
    
'*******************************************
' END OF FormatColRowOver5:
'*******************************************
    
' NOT USED 6dec08
' In gosub ConvNote, see
'    Call Note2Abc(A$, Row, gbAlto3, B$, Err)
CheckRow3AltoClef:
    If gbAlto3 Then
        gbAlto3 = False ' 12oct08
        RowDecrement = 0
    Else ' If Not gbAlto3 Then
        ' Redo row 3 as a comment line
        ' to print out a tenor
        ' line for the alto clef, but
        ' do not adjust the notes up
        ' by two octaves in sub Note2Abc.
        ' Then in JEDAbc, I can manually
        ' uncomment out the Alto3 line
        ' and print it in the Alto clef.
        gbAlto3 = True
        'Row = Row - 1 ' Row 2
        RowDecrement = 1 ' to redo Row 2 6dec08
    End If
    Return

'*******************************************
' END OF PrintNoteLoop:
'*******************************************

Err2:
    bErr = True
    gInputErrs = gInputErrs + 1
    GoSub GetNoteNo
    E$ = "WriteAbcNotes:E2 " & _
        "invalid note=" & A$ & _
        " at note no=" & B$ & _
        " in SongNo=" & pSongno & " " & Err
    Call DisplayText(E$)
    Debug.Print E$
    Return

'*******************************************

GetNoteNo:
    SaveRow = Row
    grdTable4P.Row = 1 ' note nos
    B$ = grdTable4P.Text
    grdTable4P.Row = SaveRow
    Return

'*******************************************

' BEGIN OF NOT USED #1
           'Case 6
           '    Print #pFileNo, "w:RNum~";
           'Case 7
           '    Print #pFileNo, "w:FigB~";
           'Case 8
           '    Print #pFileNo, "w:Vari~";
           'Case 9
           '    Print #pFileNo, "w:Cand~";
           'Case 10
           '    Print #pFileNo, "w:Crd~";
           'Case 11
           '    Print #pFileNo, "w:Ntvl~";
           'Case 12
           '    Print #pFileNo, "w:Lyrl~";
           'Case 13
           '    Print #pFileNo, "w:Lyr2~";
           'Case 14
           '    Print #pFileNo, "w:Lyr3~";
           'Case 15
           '    Print #pFileNo, "w:Lyr4~";
' END OF NOT USED #1

'*******************************************

' BEGIN OF NOT USED #2
       'If Row >= 1 And Row < 4 Then
       '    Print #pFileNo, VHdr(Row);
       'ElseIf Row = ROW4 Then
       '    If gbAlto3 Then
       '        Print #pFileNo, VHdr(Row);
       '    Else
       '        Print #pFileNo, VHdr(Row);
       '    End If
       'ElseIf Row > 5 Then
       '    Print #pFileNo, "w:    ";
       'Else ' Row=5
       '    Print #pFileNo, "%      ";
       'End If
' END OF NOT USED #2

End Sub


Private Sub WriteAbcNotesBass4(pRow As Integer, _
                        pCol As Integer, _
                        pA$, _
                        pBegSlur$, _
                        pEndSlur$, _
                        pBass4Tie$, _
                        pNewA$)

    ' called by WriteAbcNotes

    ' calls sub DisplayText
    ' calls sub PrintNotes (commented out)
    ' calls sub Note2Abc
    ' calls sub Time2Abc
    ' calls function fcnRepChar

    ' Globals used:
    ' Globals set:
    ' Parameters returned:
        ' pEndSlur$
        ' pBass4Tie$
        ' pNewA$

    ' See sub StoreTM.
    ' The TM: input card must contain all the
    ' time subdivisions for any and all voices.
    ' The T1: to T4: input cards contain the time
    ' for each note for the soprano to bass voices.

    ' There is a problem.  The abcm2ps program
    ' treats the lyric line
    '   w: word1 word2 ...
    ' as belonging to the Vn: line immediately
    ' above it, so as to support the singers.
    '    I use 5 or more w: lines to display harmonic
    ' analysis for the vertical chords above each
    ' word on the w: line.  Therefore if the Bass
    ' voice note times do not agree with the TM:
    ' input card consolidated note times, then
    ' the harmonic analysis vertical data will not
    ' line up properly.
    '   E.g. if the TM: card contains a measure
    ' of a 1/4 note followed by two 1/8 notes where
    ' the Bass line contains two 1/4 notes, then
    ' abcm2ps will only print the harmonic
    ' analysis for two notes in that measure.
    '   My solution is to convert the first
    ' 1/4 note in the Bass voice to two 1/8 notes
    ' tied together. Then abcm2ps will print info
    ' for three notes in that measure.
    '   Another solution is to print harmonic
    ' analysis only for the Bass notes.
    '   If no harmonic analysis is needed for a
    ' score or individual part, for performing,
    ' then the original Bass notes can be printed.

    ' My Convention: use Const's instead of
    ' constants so that it is easy to
    ' update the row numbers.
    Const ROW4 As Integer = 4 ' Bass
    Const ROW5 As Integer = 5 ' TM: or length
    Const ROW20 As Integer = 20 ' Bass
    Const ROW21 As Integer = 21 ' Bass4
    Const ROW24 As Integer = 24 ' Lyric row 4

    Dim SaveRow As Integer
    Dim SaveCol As Integer
    Dim bNewTiedNote As Boolean
    Dim bMinus As Boolean
    Dim MinusSign As Integer
    Dim MinusPos As Integer
    Dim I As Integer
    Dim TmRow5$
    Dim A$
    Dim B$
    Dim Bass4Beam$
    Dim Numer As Integer
    Dim Denom As Integer
    Dim Fraction As Integer
    Dim NewEndSlur$
    Dim NewBegSlur$
    
'*******************************************
'*******************************************

Main:
    If pCol = 138 Or pCol = 139 Then
        If pRow = 4 Then
            I = I
        End If
    End If
    GoSub CheckBassRowMinus
        'GoSub CheckMinusThisCol
            'GoSub UpdateRow21
                'GoSub GetTmRow5
            'GoSub GetTiedBassNoteName
            'GoSub CheckMinusNextCol
            'GoSub AddTie
    Exit Sub

'*******************************************
' END OF Main:
'*******************************************

    ' Compare the time for this Bass note
    ' on the consolidated TM: card line (Row 4)
    ' with the Bass T4: card line (Row 20).
    ' If equal, return.
    '   If not, then e.g. some
    ' other Voice (S, A, T) has two 1/8 notes
    ' where the Bass Voice has a 1/4 or longer.
    ' In this case, the program abcm2ps will
    ' print my 4Part harmonic analysis comments
    ' in the w: lines only where the Bass Voice
    ' has a note.
    '   To prevent this problem, convert the
    ' Bass Voice 1/4 to two tied 1/8 notes.
    ' It doesn't look pretty, but it works.
    ' Store the override values in
    '   grdTable4P.Row=21
    ' which has label Bass4.

' A minus in rows 17-20 means that a NHT is
' in that col. E.g. E,-1 means that the last
' good 4 part chord is back one col.

' An N in rows 17-20 means that the corresponding
' row 1-4 iS A NHT, e.g. E,4,N

CheckBassRowMinus:
    pNewA$ = pA$
    GoSub CheckMinusThisCol
    If bMinus Then
        bNewTiedNote = True
        GoSub UpdateRow21
        GoSub GetTiedBassNoteName
        GoSub UpdateLyricRow24
        GoSub CheckMinusNextCol
        If bMinus Then ' one more tied note
            GoSub AddTie
        Else ' last tied note
            'pBegSlur$ = NewBegSlur$
            pEndSlur$ = NewEndSlur$
            pBass4Tie$ = ""
        End If
    Else
        bNewTiedNote = False
        GoSub CheckMinusNextCol
        If bMinus Then
            ' Begin tied notes for Bass Voice.
            GoSub AddTie
            GoSub UpdateRow21
        Else ' move the T4: row time to Bass4
            pBass4Tie$ = ""
            GoSub UpdateRow21
        End If
    End If
    Return

'*******************************************
' Get the negative number starting at the minus.
CheckMinusThisCol:
    SaveRow = pRow
    grdTable4P.Row = SaveRow + 16 ' rows 17-20
    MinusSign = InStr(grdTable4P.Text, "-")
    If MinusSign > 0 Then
        bMinus = True
        MinusPos = _
            Val(Mid$(grdTable4P.Text, MinusSign))
    Else
        bMinus = False
    End If
    grdTable4P.Row = SaveRow
    Return
       
'*******************************************

    ' Concat the time value, e.g. Q or E>>
    ' or S<<, from row 5
    ' with the position value from row 20.
    ' and save at row 21, the new Bass4 row.
UpdateRow21:
    GoSub GetTmRow5
    grdTable4P.Row = ROW20 ' Label is "T4, pos"
    I = InStr(grdTable4P.Text, "~")
    If I > 0 Then
        TmRow5$ = TmRow5$ & "~"
    End If
    I = InStr(grdTable4P.Text, ",")
    If I > 0 Then
        TmRow5$ = TmRow5$ & _
            Mid$(grdTable4P.Text, I) ' ",pos"
    End If
    grdTable4P.Row = ROW21
    grdTable4P.Text = TmRow5$
    grdTable4P.Row = pRow ' reset
    Return

'*******************************************

' If yu have built a new "tied and split" note,
' then extend the previous lyric word by one
' syllable. 1oct08

UpdateLyricRow24:
    If pCol > 1 Then
        grdTable4P.Col = pCol - 1
        grdTable4P.Row = ROW24 ' Label is "Lyric4"
        If grdTable4P.Text <> "" Then
            grdTable4P.Col = pCol ' reset
            grdTable4P.Text = "_"
        End If
    End If
    Return
       
'*******************************************

' Get the length char (W H Q E S T) in the time
' field from the TM: input card,
' with any > or < chars following it, or
' ABC format length field, e.g. E3/2
GetTmRow5:
    grdTable4P.Row = ROW5 ' consolidated TM: card
    TmRow5$ = UCase$(grdTable4P.Text) ' e.g. E3/2
    'GoSub CheckBass4Beaming
    grdTable4P.Row = pRow ' reset to current row=4
    Return

'*******************************************

' 2aug08
' The ABC program abcm2ps will print two notes as beamed
' if they are next to each other and if they are eighth
' notes or less in value.  If it finds two notes together
' are longer in value, it will not beam them.
' However, to be clean, I check here for cases where
' there should be beaming and I only flag thoses notes
' with Bass4Beam$ = "~"
CheckBass4Beaming:
    Bass4Beam$ = ""
    A$ = Left$(TmRow5$, 1)
    If A$ = "S" _
    Or A$ = "T" Then
        Bass4Beam$ = "~"
    ElseIf A$ = "E" Then
        If Len(TmRow5$) = 1 Then
            Bass4Beam$ = "~"
        Else
            GoSub CalcFraction
            If Fraction >= 2 Then
                Bass4Beam$ = ""
            Else
                Bass4Beam$ = "~"
            End If
        End If
    End If
    Return
    
'*******************************************

CalcFraction:
    B$ = Mid$(TmRow5$, 2)
    I = InStr(B$, "/")
    Fraction = 1
    If I > 0 Then
        Numer = Val(B$)
        Denom = Val(Mid$(B$, I + 1))
        If Denom = 0 Then
            Fraction = Numer
        ElseIf Numer = 0 Then
            Fraction = 1
        Else
            Fraction = Numer / Denom
        End If
    End If
    Return
    
'*******************************************

' E.g. if a 1/4 note in the Bass voice is
' to be converted to two tied 1/8 notes,
' then get the tied note name, by going back
' the negative MinusPos cols,
' to pass pNewA$ to ConvNote.
GetTiedBassNoteName:
    grdTable4P.Row = ROW4
    grdTable4P.Col = pCol + MinusPos
    pNewA$ = grdTable4P.Text
    GoSub SlurNotes
    grdTable4P.Row = pRow ' reset
    grdTable4P.Col = pCol ' reset
    Return

'*******************************************

' In case the new tied and split note has
' pNewa$ = "B2)" or "(E2"
SlurNotes:
    NewBegSlur$ = ""
    NewEndSlur$ = ""
    If Left$(pNewA$, 1) = "(" Then
        NewBegSlur$ = "("
        pNewA$ = Mid$(pNewA$, 2)
    End If
    
    If Right$(pNewA$, 1) = "-)" Then
        NewEndSlur$ = "-)"
        pNewA$ = Left$(pNewA$, Len(pNewA$) - 2)
    ElseIf Right$(pNewA$, 1) = ")" Then
        NewEndSlur$ = ")"
        pNewA$ = Left$(pNewA$, Len(pNewA$) - 1)
    ElseIf Right$(pNewA$, 1) = "-" Then
        NewEndSlur$ = "-"
        pNewA$ = Left$(pNewA$, Len(pNewA$) - 1)
    End If
    Return

'*******************************************

' Return Minus=True if there is a minus "-"
' in the next col for this row 17-20
CheckMinusNextCol:
    If pCol < grdTable4P.Cols - 1 Then
        SaveCol = pCol
        grdTable4P.Col = pCol + 1
        GoSub CheckMinusThisCol
        grdTable4P.Col = SaveCol
    Else
        bMinus = False ' no more cols
    End If
    Return

'*******************************************

AddTie:
    ' Add a Tie to the Bass Voice.
    ' Also see Gosub SlurNotes.
    If pEndSlur$ = "-)" Then
        pEndSlur$ = ")" ' drop dupe
    ElseIf pEndSlur$ = "-" Then
        pEndSlur$ = "" ' drop dupe
    End If
    GoSub GetTmRow5
    GoSub CheckBass4Beaming
    pBass4Tie$ = "-" & Bass4Beam$
    Return

End Sub

Private Sub WriteAbcSong(pFileNo As Integer, _
                        pLineOut As Integer, _
                        pbFirstSong As Boolean)
    
    ' called by WriteAbc

    ' Globals used: grdKey.Col, grdKey.Row,
    '   gKeyMode, grdKeySig.Col, grdKeySig.Row
    '   gAbcHdr(I)
    ' Globals set:

    'Dim FileNo As Integer
    Dim Prefix As String
    Dim File As String
    'Dim LineOut As Integer
    Dim J As Integer
    Dim nSpaces As Integer
    Dim Row As Integer
    Dim Col As Integer
    Dim SaveRow As Integer
    Dim VHdr As Variant ' SOPRANO To BASS
    Dim A$
    Dim B$
    Dim C$
    Dim MeterLeng As Integer
    Dim NoteLeng$
    Dim AbcLeng$
    Dim Err As String
    Dim bErr As Boolean
    Dim E$
    Dim BegSlur$
    Dim EndSlur$
    'Dim Rest$      TODO not needed
    Dim NumRests As Integer
    'Dim bMinus As Boolean

    Dim I As Integer
    Dim Counter As Integer

'*******************************************
'*******************************************

Main:
    GoSub Init
    GoSub PrintSongLevel
        'GoSub OutputSongXT
        'GoSub OutputSongAbcHdr
        'GoSub OutputSongK
        'GoSub OutputSongKeySig
        'GoSub OutputSongML
    Exit Sub

'*******************************************
' END OF Main:
'*******************************************

Init:
    VHdr = Array("", "[V: S] ", "[V: A] ", _
                    "[V: T] ", "[V: B] ")
    Return

'*******************************************

PrintSongLevel:
    GoSub OutputSongXT
    If pbFirstSong Then
        GoSub OutputSongAbcHdr
    End If
    GoSub OutputSongML
    GoSub OutputSongK
    GoSub OutputSongKeySig
    Return

'*******************************************

OutputSongXT:
    If pbFirstSong Then
        A$ = ""
    Else
        Print #pFileNo, "%"
        A$ = "% "
    End If
    Print #pFileNo, A$ & "X:"; txtSongNum.Text
    pLineOut = pLineOut + 1
    Print #pFileNo, A$ & "T:"; txtDesc.Text
    pLineOut = pLineOut + 1
    Return

'*******************************************

OutputSongML:
    If pbFirstSong Then
        Print #pFileNo, "M:"; txtMeter.Text
        pLineOut = pLineOut + 1
        Print #pFileNo, "L:"; txtLength.Text
        pLineOut = pLineOut + 1
    End If
    Return

'*******************************************

OutputSongAbcHdr:
    ' Write out to the ABCPlus file
    '   filename.ABC
    ' the ABCPlus header lines saved
    ' IN gAbcHdr() from the input file
    '   filename.txt
    For I = 1 To gAbcHdrLast
        Print #pFileNo, gAbcHdr(I)
        pLineOut = pLineOut + 1
    Next I
    'GoTo Loop1
    Return

'*******************************************

OutputSongK:
    grdKey.Col = 0
    grdKey.Row = 0
    Select Case gKeyMode
        Case MAJOR
            A$ = "K:" & grdKey.Text
            GoSub ConcatSongs
        Case MINORNAT, MINORHARM, MINORMELUP
            ' no space before m or ABC may complain
            A$ = "K:" & grdKey.Text & "m"
            GoSub ConcatSongs
        Case DORIAN
            A$ = "K:" & grdKey.Text & " dorian"
            GoSub ConcatSongs
        Case PHRYGIAN
            A$ = "K:" & grdKey.Text & " phrygian"
            GoSub ConcatSongs
        Case LYDIAN
            A$ = "K:" & grdKey.Text & " lydian"
            GoSub ConcatSongs
        Case MIXOLYDIAN
            A$ = "K:" & grdKey.Text & " mixolydian"
            GoSub ConcatSongs
    End Select
    pLineOut = pLineOut + 1
    Return

'*******************************************

' In order to concatenate more than one 4Part song
' into an ABC song file, you need to enclose the
' K:x key names after the first song
' in "inline" brackets for both the treble and the
' bass clefs for four voices.  E.g.
'   [V:S] [K:C#]

' To concatenate songs together in ABCPlus with
' different times and tempos, use
'   [V:T] [K:F#m] [M:4/4] [L:1/4] [Q:1/4=90]
ConcatSongs:
    If pbFirstSong Then
        Print #pFileNo, A$
    Else
        B$ = "[" & A$ & "]"
        B$ = B$ & " [" & "M:" & txtMeter.Text & "]"
        I = InStr(txtLength.Text, "@WN=")
        If I = 0 Then
            C$ = txtLength.Text
        Else
            C$ = Left$(txtLength.Text, I - 1)
        End If
        B$ = B$ & " [" & "L:" & C$ & "]"
        B$ = B$ & " [" & "Q:" & txtTempo.Text & "]"
        
        Print #pFileNo, VHdr(SOPRANO) & B$
        Print #pFileNo, VHdr(ALTO) & B$
        Print #pFileNo, VHdr(TENOR) & B$
        Print #pFileNo, VHdr(BASS) & B$
    End If
    Return

'*******************************************

OutputSongKeySig:
    Print #pFileNo, "% Key Sig:";
    grdKeySig.Col = 0
    For Counter = 1 To gKeySigLast
        grdKeySig.Row = Counter - 1
        ' TODO ???
        Print #pFileNo, " "; grdKeySig.Text;
    Next Counter
    Print #pFileNo,
    pLineOut = pLineOut + 1
    Return

'*******************************************
' END OF PrintSongLevel:
'*******************************************

End Sub


Private Sub DisplayText(pMsg As String)
    ' called by many subs
    Static PrevMsg
    Dim CR As String
    CR = Chr$(13)
    If PrevMsg = "" Then
        txtPrint.Text = pMsg
    Else
        txtPrint.Text = PrevMsg & Chr$(10) & Chr$(13) & pMsg
    End If
    PrevMsg = pMsg
End Sub

Private Function fcnRepChar(pText As String, _
                        pFrom As String, _
                        pTo As String) As String
    ' called by sub WriteAbc
    ' Globals used:
    ' Globals set:
    ' Repeatedly replace any single character pFrom
    ' in string pText with string pTo, which can be
    ' blank or null, and return pText.
    Dim I As Integer
    Dim Text$
    Dim A$
    For I = 1 To Len(pText)
        A$ = Mid$(pText, I, 1)
        If A$ = pFrom Then
            Text$ = Text$ & pTo
        Else
            Text$ = Text$ & A$
        End If
    Next I
    fcnRepChar = Text$
    Exit Function
End Function

Private Function fcnCountChar(pText As String, _
                        pChar As String) As Integer
    ' called by sub WriteAbc
    ' Globals used:
    ' Globals set:
    ' Repeatedly replace any single character pFrom
    ' in string pText with string pChar, which can be
    ' blank, and return pText.
    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim Text$
    Dim A$

      K = 0
      I = InStr(pText, pChar)
      Do While I > 0
            K = K + 1
            If I < Len(pText) Then
                  J = InStr(Mid$(pText, _
                        I + 1), pChar)
                  If J > 0 Then
                        I = I + J
                  Else
                        I = 0
                  End If
            Else
                  I = 0
            End If
      Loop
      fcnCountChar = K
      Exit Function

End Function

Private Function fcnGridNote(pNoteX As Integer) _
                As Integer
                        
    ' called by sub ChoosePrevChord
    ' called by sub SetNoteFlags
    ' Globals used: gNoteX, gSongCurr
    ' Globals set:
    
    ' Replace the absolute note number, gNoteX,
    ' with the note number relative to the first note,
    ' which is displayed on the grid.
    
    fcnGridNote = pNoteX - _
        gSong(gSongCurr).FirstNote + 1

End Function


Private Sub cmdWriteAll_Click()
    ' called by cmd click
    ' Globals used:
    ' Globals set: gbWriteAll =,
    '       cmdWriteAll.Caption =

    ' If gbWriteAll = False, then change the captions
    ' for cmdWriteAbc_Click and
    ' cmdWriteChord_Click to say
    ' that one song will be written and that one
    ' file of chords will be written for one key.
    ' If true, then all the songs in memory will
    ' be written to one ABC file, e.g. Asnn55.abc
    ' and all the files of all the keys will be
    ' written. e.g. CMajor.txt, C#Major, etc.

    txtPrint.Text = ""
    If Not gbWriteAll Then
        gbWriteAll = True
        cmdWriteAll.Caption = "1"
        cmdWriteAbc.Caption = "ev song to &ABC"
        cmdWriteChord.Caption = "all key files"
        cmdNextFile.Caption = "All files"
    Else
        gbWriteAll = False
        cmdWriteAll.Caption = "all"
        cmdWriteAbc.Caption = "1 song to &ABC"
        cmdWriteChord.Caption = "1 key file"
        cmdNextFile.Caption = "Next &file"
    End If
End Sub

Private Sub cmdWriteChord_Click()
    ' called by cmd click
    txtPrint.Text = ""
    If gbWriteAll Then
        Call WriteAllChords ' for this gKeyMode
    Else
        Call WriteDblTypes
    End If
End Sub

Private Sub cmdWriteCsv_Click()
    ' called by click
    Call WriteCsv(gSongCurr)
End Sub
Private Sub WriteCsv(pSongno As Integer)
    ' called by cmdPrint_click
    ' Globals used: gPrefix, gDirSongs, gFile
    ' Globals set:
    
    ' See sample in \4part\songs\Lomond5.CSV

    Dim FileNo As Integer
    Dim Prefix As String
    Dim File As String
    Dim LineOut As Integer
    Dim I As Integer
    Dim Counter As Integer
    Dim Row As Integer
    Dim Col As Integer
    Dim E$

    'Prefix = "C:\vb4prg\4part\"
    Prefix = gPrefix & gDirSongs
    File = gFile & ".CSV"

    If pSongno <= 0 Or pSongno > gSongLast Then
        GoTo Err1:
    Else
        FileNo = FreeFile ' get next available #
        Open Prefix & File For Output As #FileNo
        E$ = "Opened file=" & Prefix & File
        Call DisplayText(E$)
        Debug.Print E$
        With gSong(pSongno)
            Write #FileNo, "Song No."; _
                txtSongNum.Text; _
                txtDesc.Text; ""; ""; _
                "Meter="; _
                txtMeter.Text; _
                txtLength.Text;
            grdKey.Col = 0
            For Counter = 0 To .NumKey - 1
                grdKey.Row = Counter
                Write #FileNo, grdKey.Text;
            Next Counter
            Write #FileNo,
            LineOut = LineOut + 1
            For Row = 0 To grdTable4P.Rows - 1
                grdTable4P.Row = Row
                For Col = 0 To grdTable4P.Cols - 1
                    grdTable4P.Col = Col
                    Write #FileNo, grdTable4P.Text;
                Next Col
                Write #FileNo,
                LineOut = LineOut + 1
            Next Row
        End With
        'call PrintNotes(pSongno, LineOut, Fileno)
        Close #FileNo
        E$ = "Closed file=" & Prefix & File & _
            " LineOut=" & LineOut
        Call DisplayText(E$)
        Debug.Print E$

    End If
    Exit Sub

Err1:
    E$ = "WriteCsv: " & _
        "invalid SongNo=" & pSongno
    Call DisplayText(E$)
    Debug.Print E$

End Sub

Private Sub txtMyCand_KeyPress(KeyAscii As Integer)
    ' called by Keypress
    ' calls MyCandidate
    
    ' The new logic for program 4Part allows
    ' the user to select any error or non-error
    ' candidate.
    
    ' Make MyCandidate a separate sub so that
    ' sub SaveRestoreNextSong can call it too.
    
Main:
    ' Did the user press the return key?
    If KeyAscii = vbKeyReturn Then
        Call MyCandidate
    End If
End Sub

Private Sub MyCandidate()
    ' called by txtMyCand_KeyPress
    ' called by SaveRestoreNextSong ?
    
    ' calls cmdNextSolution_Click

    ' Globals used: gNoteLast
    ' Globals set: gNoteX

    ' Make MyCandidate a separate sub so that
    ' both sub txtMyCand_KeyPress and
    ' sub SaveRestoreNextSong can call it.
    
    ' I think the original use of
    '   gNote(gNoteX).bUserChoice
    '   gNote(gNoteX).UserCand
    ' was to make the choice permanent
    ' even when going to another song and back.
    ' Now that I have changed 4Part to retain the
    ' last choices for all the notes in a song
    ' and to restore them when returning to the
    ' song, there is not the same need for
    ' holding the choice in .UserCand.
    
    ' The new logic for this program allows
    ' the user to select any error or non-error
    ' candidate.  The main difference is that
    ' 20 is added to the rank of the error-candidate
    ' to make it less likely to be selected.
    
    ' I was going to use -MyCand to indicate a
    ' "permanent" override, or to use an
    ' candidate with error.  If MyCand < 0 then store
    ' the corresponding gChord indexes in gNote
    ' where the gNote slots =0.
    ' If MyCand = 0, then zero out the negative indexes.

    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim L As Integer
    Dim Ch As Integer
    Dim MyCand As Integer
    Dim MyCandAdj As Integer
    Dim A$
    Dim E$
    
'*********************************************
'*********************************************
Main:
    GoSub Init
    GoSub MainLine
    Exit Sub

'*********************************************
'*********************************************

Init:
    txtPrint.Text = ""
    gNote(gNoteX).bUserChoice = False
    Return
    
'*********************************************
    
MainLine:
    A$ = txtMyCand.Text
    If IsNumeric(A$) Then
        With gNote(gNoteX)
            MyCand = Val(A$)
            If MyCand = 0 Or MyCand = -999 Then
                ' TODO
                'GoSub BackoutChoices
            Else
                MyCandAdj = .FirstCand - 1 + _
                    Abs(MyCand)
                If MyCandAdj < .FirstCand _
                Or MyCandAdj > .LastCand Then
                    GoSub Err1
                Else
                    GoSub MySolution
                End If
            End If
        End With
    End If
    
    'gNote(gNoteX).bUserChoice = False
    Return
    
'*********************************************
    
' Temporarily store the gChord indexes
' as negative values in the zero slots
' of the gNote indexes, as a flag.
' These negative indexes will be used as if
' they were entered in the user file
' to help find solutions to the right of it.
' See MatchInputNotes in fcnbCandChord
' Note - the user will probably not try to use
' an override for a NHT.
MySolution:
    gNote(gNoteX).bUserChoice = True
    ' TODO this bMustAllowError does not work:
    ' Allow the user to select a candidate in error
    ' if it is an allowed error, that is, "(myerr"
    ' See fcnErrAllow:
    'gNote(gNoteX).bMustAllowError = True
    
    With gNote(gNoteX)
        .UserCand = MyCandAdj
        If gCand(.UserCand).Error <> "" Then
            If Left$(gCand(.UserCand).Error, 1) <> "(" _
            Then
                gCand(.UserCand).Error = "(" & _
                    gCand(.UserCand).Error
                .nGoodCand = .nGoodCand + 1
            End If
        End If
        ' TODO
        '.nGoodCand = .nGoodCand + 1

        ' I think "MyCand < 0" allows selecting
        ' a cand in error. 11/9/6
        If MyCand < 0 Then
            Ch = gCand(.UserCand).Index2Ch
            For K = SOPRANO To BASS
                ' TODO cleanup
                If .Voice(K).Index2Name = 0 Then
                    L = gChord(Ch).Voice(K).Index2Na
                    .Voice(K).Index2Name = -L
                    .Voice(K).Name = "(" & _
                        gNoteName(L)
                End If
            Next K
        ElseIf MyCand > 0 Then
            ' TODO not needed if no negative MCand's
            'gNote(gNoteX).bUserChoice = False
            For K = SOPRANO To BASS
                ' TODO cleanup
                If .Voice(K).Index2Name < 0 Then
                    .Voice(K).Index2Name = 0
                    .Voice(K).Name = ""
                End If
            Next K
        End If
    End With
    Call cmdNextSolution_Click
    Return

'*********************************************
    
Err1:
    With gNote(gNoteX)
    J = .LastCand - .FirstCand + 1
    E$ = "MyCandidate: enter a " & _
        "candidate no. between " & _
        "1 and" & J & _
        "to change the solution for note=" & _
        gNoteX & " in Song no=" & gSongCurr
    txtPrint.Text = E$
    End With
    Return
    
'**********************************************
' NOT USED
' TODO update .Index2Name with call to fcnNoteIndex().
' Make the negative gNotex indexes = 0 again.
BackoutChoices:
    With gNote(gNoteX)
        For K = SOPRANO To BASS
            ' TODO cleanup
            If .Voice(K).Index2Name < 0 Then
                .Voice(K).Index2Name = 0
                .Voice(K).Name = ""
            End If

           'If .Voice(K).Index2Na < 0 Then
           '    .Voice(K).Index2Na = 0
           '    .Voice(K).Name = ""
           'End If

        Next K
    End With
    Call cmdNextSolution_Click
    Return

End Sub


Private Sub txtDNote_KeyPress(KeyAscii As Integer)
    ' called by Keypress
    ' Globals used: gNoteLast
    ' Globals set: gNoteX

    Dim I As Integer
    Dim A$
    Dim B$
    Dim C$
    Dim Acc$
    Dim Ltr$
    Dim Dig$
    Dim bErr As Boolean

    If KeyAscii = vbKeyReturn Then
        gDNoteVoice$ = ""
        gDNoteRequest$ = ""
        A$ = UCase$(txtDnote.Text)
        If A$ = "" Then
            GoTo Exit1
        End If
        I = InStr(A$, ",")
        If I = 0 Then
            GoSub Err1
            GoTo Exit2
        ElseIf I = 1 Then
            B$ = ""
            gDNoteVoice$ = "ALL"
            GoSub EditNoteName
        ElseIf I > 1 Then
            B$ = Left$(A$, 1)
            If B$ = "S" Or B$ = "A" Or B$ = "T" Or B$ = "B" Then
                gDNoteVoice$ = B$
            Else
                GoSub Err1
                GoTo Exit2
            End If
            GoSub EditNoteName
        End If
    End If
Exit1:
    Call DisplayCandChords
Exit2:
    Exit Sub
            
' C$ = "ALL" or e.g. #E4 or bB3 or =A2 or C5
EditNoteName:
    If Len(A$) < I + 2 Then
        GoSub Err1
        'GoTo Exit1
    Else
        C$ = Mid$(A$, I + 1, 3)
        If Len(C$) >= 3 Then
            Acc$ = Mid$(C$, 1, 1)
            Ltr$ = Mid$(C$, 2, 1)
            Dig$ = Mid$(C$, 3, 1)
        Else
            Acc$ = ""
            Ltr$ = Mid$(C$, 1, 1)
            Dig$ = Mid$(C$, 2, 1)
        End If
        GoSub EditFields
        If bErr Then
            GoSub Err1
        Else
            gDNoteRequest$ = C$
        End If
    End If
    Return
    
EditFields:
    bErr = False
    Select Case Acc$
        Case "", "#", "=", "b", "X"
            bErr = False
        Case Else
            bErr = True
            Return
    End Select
    
    If Ltr$ >= "A" And Ltr$ <= "G" Then
        bErr = False
    Else
        bErr = True
        Return
    End If
    
    If Dig$ >= "0" And Dig$ <= "9" Then
        bErr = False
    Else
        bErr = True
    End If
    
    Return

Err1:
    Debug.Print "txtDNote_KeyPress: "; _
        "enter Voice,Note to display those notes "; _
        "where Voice=S,A,T,B."
    Return

End Sub


Private Sub txtNoteNo_KeyPress(KeyAscii As Integer)
    ' called by Keypress
    ' Globals used: gNoteLast
    ' Globals set: gNoteX

    Dim I As Integer
    txtPrint.Text = ""
    txtMyCand.Text = ""

    If KeyAscii = vbKeyReturn Then
        If IsNumeric(txtNoteNo.Text) Then
            I = Val(txtNoteNo.Text)
            If I <> gNoteX Then
                If I <= 0 Then
                    gNoteX = 1
                ElseIf I > gNoteLast Then
                    gNoteX = gNoteLast
                Else
                    gNoteX = I + _
                        gSong(gSongCurr).FirstNote
                End If
                Call cmdPrevNote_Click
            End If
        Else
            Debug.Print "txtNoteNo_KeyPress: "; _
                "enter a number to change note"
        End If
    End If
End Sub



Private Sub cmdNextSong_Click()
    ' called by Click
    ' called by txtSongNum_KeyPress
    ' called by WriteAbc
    
    ' calls sub SaveRestoreNextSong()
    
    ' Globals used: gSongLast, gSongNext
    ' Globals set: gSongCurr =, gSongNext = 0

    ' gSongNext is only set by txtSongNum_KeyPress.
    
    Dim Txt$
    Dim I As Integer
    
    txtPrint.Text = ""
    txtMyCand.Text = ""
    If gSongCurr = 0 Then
        I = I
    End If
    If gSongLast > 0 Then
        ' Save the .BestCand for each note in this song
        Call SaveRestoreNextSong("save")
        If gSongNext > 0 Then
            gSongCurr = gSongNext
            gSongNext = 0
        Else
            If gSongCurr < gSongLast Then
                gSongCurr = gSongCurr + 1
            Else
                gSongCurr = 1
            End If
        End If
        Call DisplaySong(gSongCurr)
        Call DisplayNotes(gSongCurr)
        Call cmdHarmony_Click
        Txt$ = txtPrint.Text
        ' Restore the .BestCand for each note in this song
        Call SaveRestoreNextSong("restore")
        If txtPrint.Text = "" Then
            txtPrint.Text = Txt$
        End If
        'call DisplayHarm(gSongCurr)
        'gbShowHarmony = True
        'cmdShow.Caption = "&Show input"
    End If
End Sub

Private Sub txtSongNum_KeyPress(KeyAscii As Integer)
    ' called by Keypress
    ' calls cmdNextSong_Click
    
    ' Globals used: gNoteLast
    ' Globals set: gSongNext

    ' The user has entered a song number to change to
    ' in the Song number box on upper right screen.
    ' Edit the number.  Scan all the songs for that
    ' .X number.  The next song to change to, gSongNext,
    ' is the index of that song.
    ' Sub cmdNextSong_Click uses gSongNext if not 0.
    
    ' Note - this sub is entered for each key press but
    ' the body of the code is not done until the
    ' ENTER key if pressed:
    '   KeyAscii = vbKeyReturn
    
    Dim I As Integer
    Dim Song As Integer
    
    If KeyAscii = vbKeyReturn Then
        txtPrint.Text = ""
        gSongNext = 0
        If Not IsNumeric(txtSongNum.Text) Then
            GoSub Debug1
        Else
            I = Val(txtSongNum.Text)
            For Song = 1 To gSongLast
                If gSong(Song).X = I Then
                    gSongNext = Song
                    Exit For
                End If
            Next Song
            If gSongNext = 0 Then
                GoSub Debug1
            Else
                Call cmdNextSong_Click
            End If
        End If
    End If
    Exit Sub

Debug1:
    'Debug.Print "txtSongNum_KeyPress: "; _
    '    "enter a song number to change to"
    txtPrint.Text = "txtSongNum_KeyPress: " & _
        "enter a song number to change to"
    Return

' TODO not used
            If I <> gSongCurr Then
                If I <= 0 Then
                    gSongCurr = 1
                ElseIf I > gSongLast Then
                    gSongCurr = gSongLast
                Else
                    gSongCurr = I - 1
                End If
                Call cmdNextSong_Click
            End If

End Sub



Private Sub cmdPrevNote_Click()
    ' called by sub Click
    ' called by SaveRestoreNextSong
    ' calls sub DisplayCandChords
        
    ' Globals used: gSongCurr, gNoteLast,
    '       gSong(gSongCurr), gNote(I).LastCand
    ' Globals set: gNoteCurr =, gNoteX =
    Dim I As Integer

    txtPrint.Text = ""
    txtMyCand.Text = ""
    ' Display the candidate chords for the
    ' next note that has at least one
    ' candidate chord.  Skip over the notes
    ' at a single or double measure bar.
    With gSong(gSongCurr)
        'If gSongCurr = 4 Then Stop
        gNoteCurr = gNoteX
        If gNoteLast > 0 Then
            If gNoteCurr > 1 Then
                For I = gNoteCurr - 1 To _
                .FirstNote Step -1
                    If gNote(I).LastCand > 0 _
                    Then
                        GoTo Found
                    End If
                Next I
                For I = .LastNote To _
                gNoteCurr + 1 Step -1
                    If gNote(I).LastCand > 0 _
                    Then
                        GoTo Found
                    End If
                Next I
            Else
                For I = .LastNote To .FirstNote Step -1
                    If gNote(I).LastCand > 0 _
                    Then
                        GoTo Found
                    End If
                Next I
            End If
        End If
    End With
    Exit Sub
Found:
    gNoteCurr = I
    gNoteX = gNoteCurr
    Call DisplayCandChords
End Sub

Private Sub cmdPrevSong_Click()
    ' called by sub Click
    ' Globals used: gSongLast
    ' Globals set: gSongCurr =
    Dim I As Integer
    Dim J As Integer
    Dim Fn As Integer
    Dim Ln As Integer
    Dim Note As Integer
    Dim Index1 As Integer
    Dim SaveIndex As Integer
    Dim Txt$

    txtPrint.Text = ""
    txtMyCand.Text = ""
    If gSongLast > 0 Then
        ' Save the .BestCand for each note in this song
        Call SaveRestoreNextSong("save")
        If gSongCurr > 1 Then
            gSongCurr = gSongCurr - 1
        Else
            gSongCurr = gSongLast
        End If
        Call DisplaySong(gSongCurr)
        Call DisplayNotes(gSongCurr)
        Call cmdHarmony_Click
        Txt$ = txtPrint.Text
        ' Restore the .BestCand for each note in this song
        Call SaveRestoreNextSong("restore")
        If txtPrint.Text = "" Then
            txtPrint.Text = Txt$
        End If
        'call DisplayHarm(gSongCurr)
        'gbShowHarmony = True
        'cmdShow.Caption = "&Show input"
    End If
    Exit Sub
    
End Sub

Private Sub cmdShow_Click()
    ' called by cmd click
    ' Globals used: gSongCurr
    ' Globals set: gbShowHarmony =,
    '       cmdShow.Caption =

    ' note should reset when moving to a new song.

    txtPrint.Text = ""
    If Not gbShowHarmony Then
        gbShowInputNotesOnly = False
        Call cmdHarmony_Click
        'call DisplayHarm(gSongCurr)
        'gbShowHarmony = True
        'cmdShow.Caption = "&Show input"
    Else ' gbShowHarmony = True
        gbShowInputNotesOnly = True
        Call DisplayNotes(gSongCurr)
        gbShowHarmony = False
        cmdShow.Caption = "&Show harmony"
    End If
End Sub

Private Sub Command2_Click()
    ' called by cmd click
    ' Only used as an example.

    ' Test works.  This displays the row and col
    ' that the mouse cursor has moved to,
    ' after the first grid square.
    Debug.Print "Command2_Click: "; _
        "grid Extra row, col="; _
        grdExtra.Row; grdExtra.Col
End Sub

Private Sub Form_Load()
    Call Init4Part
    Call Main4Part
    ' Don't End if you want to see the filled form
    'End
End Sub




Private Sub Init4Part()
    ' called by sub Form_Load

    ' calls sub SetGrid4PartLabels
    ' calls sub SetGridExtraLabels
    ' calls sub InitScales ' only once
    ' calls InitDoubleNames ' call only once
    ' calls sub InitAbcTable ' only once

    Dim I As Integer
    Dim Ctr As Integer
    Dim A$

    'gBassGap = BASSGAPMAX ' 36
    gBassGap = BASSGAPDEFAULT ' 24
    gMinVarianceForDupes = 2#
    gBarBeg = 0
    gBarEnd = BARSMAX
    
    Call SetGrid4PartLabels ' upper solution panel
    Call SetGridExtraLabels ' lower candidates panel
    
    ' Allow all Roman numerals as chords
    ' See Roman Numerals in sub StoreOptionInclude.
    For I = 1 To TRIADALLOWEDMAX
        gTriadAllowed(I) = True
    Next I

    ' Allow all Figured Bass types, e.g. 53, 6, etc
    ' See the types in sub StoreOptionFigBass.
    For I = 0 To FIGBASSALLOWEDMAX
        gbFigBassAllowed(I) = True
    Next I

    gxErrorAllowed = 0 ' allow none of these errors
    gxErrorAllowedPass1 = 0 ' allow none of these errors
    ' Used in sub StoreOptionAllow
    ' WARNING - must keep this array in the same
    '           order as the Const's P5 thru User,
    '           each of which is calc'd from prev.
    ' Note an error name cannot match the last
    ' characters of another error name,
    ' e.g. cannot use Inv because it matches 2Inv.
    gErrNameLC = Array("", "P5", "P8", _
        "Hid5", "Hid8", _
        "ProgNv", "Prog63", "PrgAny", _
        "PrgBas", "PrgExt", "Prg6th", "Prg7th", _
        "NotFin", "Sim4", "BxT", "TxA", "AxS", _
        "Invr", "Not7th", "User", "2Inv", _
        "X7th", "BRange", "Above", "BassGap", _
        "Tie4", "Rest", "Dupe", "Increm", "P4Int", _
        "FBopt", "Triple") ' "Pairs")
    gErrName = Array("", "P5", "P8", _
        "HID5", "HID8", _
        "PROGNV", "PROG63", "PRGANY", _
        "PRGBAS", "PRGEXT", "PRG6TH", "PRG7TH", _
        "NOTFIN", "SIM4", "BXT", "TXA", "AXS", _
        "INVR", "NOT7TH", "USER", "2INV", _
        "X7TH", "BRANGE", "ABOVE", "BASSGAP", _
        "TIE4", "REST", "DUPE", "INCREM", "P4INT", _
        "FBOPT", "TRIPLE") ' "PAIRS")

    ' String together all the items in gErrName
    ' into the string gErrorNames$, with each name
    ' preceded by a two digit number from 01 to 31.
    ' The number dd will be used to build 2 ^ dd,
    ' an error bit in gxErrorAllowed, etc.
    ' The gErrorNames$ string can be scanned with
    ' Instr faster than passing the array.
    For I = 1 To ERRNAMEMAX
        A$ = Right$(Str$(100 + I), 2)
        gErrorNames$ = gErrorNames$ & A$ _
            & gErrName(I) & " "
    Next I
    'Debug.Print gErrorNames$
    'Stop

    ' Do this before "RN:"
    If Not gbScalesSetup Then
        Call InitScales ' only once
    End If
    Call InitDoubleNames ' call only once

    ' unused subs from VB4 exercise.
    'SetColWidth
    'SetRowHeight
    Call InitAbcTable ' call only once
End Sub


Private Sub SetGrid4PartLabels()
    ' called by sub Init4Part
    ' called by sub DisplayNotes.
    Dim Counter As Integer
    Dim Texts As Variant

    grdTable4P.ColWidth(0) = 700
     ' Set the current row to row #0.
    grdTable4P.Row = 0
    ' Write into Row #0, Col #1 to the end
    For Counter = 1 To grdTable4P.Cols - 1
        grdTable4P.Col = Counter
        grdTable4P.Text = Counter
    Next Counter

    ' Use Variant = Array(...) instead of READ DATA
    ' The upper Grid lines run from 0-24.
    ' Row 0 is for the column labels.
    ' Col 0 is for the Row labels.
    Texts = Array("", "Soprano", "Alto", "Tenor", _
                "Bass", "Length", "RomNum", _
                "FigBass", "Var/Rank", "Err/Cand", _
                "1/2step", "Intervls", "Outer/Inr", _
                "NHT", "Lyric1", "Lyric2", _
                "Chord#", "T1, Pos", "T2, Pos", _
                "T3, Pos", "T4, Pos", "Bass4", _
                "gAfterX", "Lyric3", "Lyric4")
    
    '           "B:tas", "Tas As")
    ' Set the current col to col #0.
    grdTable4P.Col = 0
    ' Write into Col #0, Row #1 to the end
    For Counter = 1 To grdTable4P.Rows - 1
        grdTable4P.Row = Counter
        grdTable4P.Text = Texts(Counter)
    Next Counter
End Sub

Private Sub SetGridExtraLabels()
    ' called by sub Init4Part
    ' called by sub DisplayCandChords
    Dim Counter As Integer
    Dim Texts As Variant

     ' Set the current row to row #0.
    grdExtra.Row = 0
    ' Write into Row #0, Col #1 to the end
    For Counter = 1 To grdExtra.Cols - 1
        grdExtra.Col = Counter
        grdExtra.Text = Counter
    Next Counter

    ' Use Variant = Array(...) instead of READ DATA
    Texts = Array("", "Soprano", "Alto", "Tenor", _
                "Bass", "RomNum", "FigBass", _
                "Variance", "Error", "Rank", _
                "Outr,Inr", "Chord#")
    grdExtra.ColWidth(0) = 700
    ' Set the current col to col #0.
    grdExtra.Col = 0
    ' Write into Col #0, Row #1 to the end
    For Counter = 1 To grdExtra.Rows - 1
        grdExtra.Row = Counter
        grdExtra.Text = Texts(Counter)
    Next Counter
    Exit Sub
End Sub



Private Sub Main4Part()
    ' called by sub Form_Load

    ' calls sub InitNoteNamesCMajor
    ' calls sub InitRangeNames
    ' calls sub InputFileName
    ' calls sub InputLines
    ' calls sub DisplaySong(gSongCurr)
    ' calls sub DisplayNotes(gSongCurr)
    ' calls sub cmdHarmony_Click

    ' Globals used: gInputErrs
    ' Globals set: gSongCurr, gbShowHarmony,
    '   cmdShow.Caption
    Dim I As Integer
    Dim K As Integer
    Dim bErr As Boolean

'*********************************************
'*********************************************
Main:
    GoSub Init
        'call InitNoteNamesCMajor
        'call InitRangeNames
    If Not gbFileNameNext Then
        GoSub GetFileName
            'call InputFileName
    End If
    If Not bErr Then
        GoSub GetInputLines
            'call InputLines
        If Not bErr Then
            GoSub MainLine
                'gSongCurr = 1
                ' calls sub DisplaySong
                ' calls sub DisplayNotes
                ' calls sub cmdHarmony_Click
        End If
    End If
    Exit Sub

'*********************************************
'*********************************************

Init:
    'gbDebugPrint = True
    ' TODO not needed. Here for clarity.
    gSongCurr = 0 ' for cmdNextFile
    'gNoteLast = 0 ' so that first note used = 1.
    gNoteLast = 1 ' so that first note used = 1.
    gSongLast = 1 ' so that first song used = 1.
    gNumGoBacks = 0
    gNumGoBacksMax = GOBACKSMAX ' 1000
    gNumGoBacksMod = GOBACKSMOD ' 100
    gBarsPerLine = BARSPERLINE ' =2 see Sub InterleaveAbcLines
    gMaxNextBest = MAXNEXTBEST ' default=2
    
    ' Build table gNoteNameCMajor to handle
    ' OP: RANGE BASS F2-G4 etc
    ' which are specified in note of C Major.
    Call InitNoteNamesCMajor ' init once
    Call InitRangeNames ' init once
    Return

'*********************************************

GetFileName:
    Call InputFileName ' input the file which
    ' contains the names of the input files.
    ' All the subdirectory names except one are
    ' commented out.
    If gInputErrs > 0 Then
        bErr = True
        GoSub Err1
    Else
        bErr = False
    End If
    Return

'*********************************************

GetInputLines:
    Call InputLines ' input the song(s)
    'GoSub Debug1
    If gInputErrs > 0 Then
        bErr = True
        GoSub Err1
    Else
        bErr = False
    End If
    Return

'*********************************************

MainLine:
    ' set by OP: MISC HARMONY OFF
    'gbShowInputNotesOnly = True ' 25sep08
    'If gSongLast = 0 Then
    If gSong(1).X = 0 Then
        GoSub Err2
    Else
        gSongCurr = 1
        Call DisplaySong(gSongCurr)
        Call DisplayNotes(gSongCurr)
        If Not gbShowInputNotesOnly Then
            Call cmdHarmony_Click
            Call DisplayHarm(gSongCurr)
            gbShowHarmony = True
            cmdShow.Caption = "&Show input"
        End If
    End If
    'gbDebugPrint = False
    Return

'*********************************************

Err1:
    Debug.Print "Main4Part: gInputErrs="; _
        gInputErrs
    Return


'*********************************************

Err2:
    Debug.Print "Main4Part: no songs found. "; _
        "gSongLast="; gSongLast
    Return

'*********************************************

' TODO cleanup
Debug1:
    For I = 1 To 90
        Debug.Print "Main4Part:"; I; _
            gNote(I).SongKey; " ";
        K = K + 1
        If K >= 5 Then
            Debug.Print
            K = 0
        End If
    Next I
    Debug.Print
    I = I
    Return

End Sub
Private Sub InputFileName()
    ' called by sub Main4Part
    
    ' Globals used: gSongLast,
    '   gSong(gSongLast).NumDoubleBars
    ' Globals set: gInputErrs

    ' Must call InputFileName before InputLines.

    ' Open a fixed file name:
    '   C:\vb4prg\4part\4PartFN.txt"

    ' Each input line contains one or two VBasic
    ' strings, separated by a comma.
    ' gPrefix, gDirChords, gDirSongs, gFile are
    ' followed by a string in double quotes.
    ' BEGIN means to save the following gDirSongs
    ' and gFile strings for use until END.
    ' If any values are missing, increment
    ' gInputErrs and return.

    Dim I As Integer
    Dim FileNo As Integer
    'Dim gFileOneName As String
    'Dim gFileOneName As String
    Dim E$
    Dim A$
    Dim B$
    Dim bEndOfFile As Boolean
    Dim bDebugPrintSave As Boolean
    Dim bInDirSong As Boolean
    Dim NumInputStrings As Integer
    Dim bInFileList As Boolean
    
'**************************************
'**************************************

Main:
    GoSub Init
    GoSub MainLine
    'GoSub Debug2
    GoSub Conclusion
    Exit Sub

'**************************************
'**************************************

Init:
    ' This is the only hard-coded filename.
    ' All the other filenames are contained in it.
    ' FILEONEPREFIX = "C:\vb4prg\4part\"
    ' FILEONENAME = "4PartFN" & ".TXT"
    gFileOnePrefix = FILEONEPREFIX
    gFileOneName = FILEONENAME
    
    FileNo = FreeFile ' get next available #

    'Open Prefix & File For Input As #FileNo
    Open gFileOnePrefix & gFileOneName _
        For Input As #FileNo

    ' ASCII values 8, 9, 10 and 13 convert to
    ' backspace, tab, linefeed and carriage return.
    Return
    
'**************************************
    
MainLine:
    GoSub GetString
    Do While Not bEndOfFile ' Loop until end of file.
        B$ = UCase$(A$)
        Select Case B$
            Case "GPREFIX"
                GoSub GetString
                gPrefix = A$
            Case "GDIRCHORDS"
                GoSub GetString
                gDirChords = A$
            Case "GDIRSONGS"
                GoSub GetString
                If bInDirSong Then
                    gDirSongs = A$
                End If
                GoSub Store1
            Case "GFILE"
                GoSub GetString
                If bInDirSong Then
                    gFile = A$
                End If
                GoSub Store1
            Case "GABCFILE"
                GoSub GetString
                If bInDirSong Then
                    gAbcFile = A$
                End If
                GoSub Store1
            Case "G4PARTFILE"
                GoSub GetString
                If bInDirSong Then
                    g4PartFile = A$
                End If
                GoSub Store1
            Case "BEGIN"
                bInDirSong = True
            Case "END"
                ' stop after first good BEGIN...END group
                If bInDirSong Then
                    bInDirSong = False
                    bEndOfFile = True
                End If
            Case "BEGINFILELIST"
                bInFileList = True
            Case "ENDFILELIST"
                If bInFileList Then
                    bInFileList = False
                    'bEndOfList = True
                End If
            Case Else ' eg blank line or xBEGIN
        End Select
        GoSub GetString
    Loop
    Return

'**************************************

' chr$(34) is a double quote "
Store1:
    If bInFileList Then
        If gFileNamesUsed < FILENAMESMAX Then
            gFileNamesUsed = gFileNamesUsed + 1
            gFileTypes(gFileNamesUsed) = B$
            gFileNames(gFileNamesUsed) = A$
                'Chr$ (34) & A$ & Chr$(34)
        Else
            GoSub Debug1
        End If
    End If
    Return
    
'**************************************

Debug2:
    Debug.Print "InputFileName: D2 "
    For I = 1 To gFileNamesUsed
        Debug.Print "D2: "; gFileTypes(I); gFileNames(I)
    Next I
    Debug.Print
    Return
    
'**************************************

Conclusion:
    GoSub CloseFile

    If gPrefix = "" Then
        B$ = "GPREFIX"
        GoSub Err1
    ElseIf gDirChords = "" Then
        B$ = "GDIRCHORDS"
        GoSub Err1
    ElseIf gDirSongs = "" Then
        B$ = "GDIRSONGS"
        GoSub Err1
    ElseIf gFile = "" Then
        B$ = "GFILE"
        GoSub Err1
    End If
    Return

'**************************************

' Get the next string field following the
' initial identifier, eg gFile,
' up to the next comma,
' or up to the end of the line.
' The first field on a card is an alpha name.
' The second field on a card is text enclosed
' in double quotes, e.g.
'   BEGIN
'   gDirSongs,"Songs\Bach\"
'   gAbcFile,"A026900"
'   g4PartFile,"B026900"
'   gFile,"026900b"
'   END

' Notice to skip over a BEGIN ... END group of
' lines, just change the spelling of BEGIN, eg
'   xBEGIN.
'Same for the BEGINFILELIST groups.

GetString:
    bEndOfFile = EOF(1)
    A$ = ""
    If Not bEndOfFile Then
        Input #FileNo, A$
        NumInputStrings = NumInputStrings + 1
    Else
        GoSub CloseFile
    End If
    Return

'**************************************

CloseFile:
        Close #FileNo    ' Close file.
        bDebugPrintSave = gbDebugPrint
        'gbDebugPrint = True
        If gbDebugPrint Then
            Debug.Print "InputFileName:E2 Opened file="; _
                gFileOneName & gFileOneName
            E$ = "InputFileName:E2 Closed file=" & gFileOneName & gFileOneName
            Call DisplayText(E$)
            Debug.Print E$
            E$ = "InputFileName:E2 # input strings=" _
                & NumInputStrings
            Call DisplayText(E$)
            Debug.Print E$
            E$ = "InputFileName:E2 gPrefix=" & gPrefix _
                & ", gDirSongs=" & gDirSongs _
                & ", gFile=" & gFile
            Call DisplayText(E$)
            Debug.Print E$
        End If
        gbDebugPrint = bDebugPrintSave
    Return

'**************************************

Err1:
    gInputErrs = gInputErrs + 1
    E$ = "InputFileName:E1 in file " & _
        gFileOneName & gFileOneName & _
        ", missing " & B$
    Debug.Print E$
    I = I
    Stop
    Return

'**************************************

Debug1:
    'gInputErrs = gInputErrs + 1 ' not really an error
    E$ = "InputFileName:D1 in file " & _
        gFileOneName & gFileOneName & _
        "reached FILENAMESMAX=" & _
        FILENAMESMAX & _
        "in BEGINFILELIST"
    Debug.Print E$
    Return
    
End Sub

Private Sub InputLines()
    ' called by sub Main4Part

    ' calls function fcnbInputLine
    ' calls sub ParseAbc
    ' calls sub StoreSong
    ' calls sub InitVoiceRangeDefaults
    ' calls sub PropagateOptions
    ' calls sub ParseTabs
    ' calls sub StoreNotes
    ' calls sub DisplayText
    ' calls sub StoreTMedit(bErr)
    ' calls sub FillWithKeys
    ' calls sub InterleaveAbcLines

    ' Globals used: gSongLast,
    '   gSong(gSongLast).NumDoubleBars
    '   gPrefix, gDirChords, gDirSongs, gFile were
    '   set in sub InputFileName
    ' Globals set: gInputLineNo, gInputErrs,

    '(1) Read in and store in an array all the
    ' ABCPlus language cards between the
    ' "%ABC BEGIN" and the "%ABC END" cards.
    '(2) Skip over the "%SKIP BEGIN" and the
    ' "%SKIP END" cards and all cards between them.
    '(3) Read in the 4Part OPTION cards, OP: in
    ' col 1, which may occur before all songs,
    ' between songs, and even within a song.
    '(4) Read in the song parm cards which have a
    ' single letter and colon in column 1.
    ' A song begins with the X:n card.
    ' The first song must be X:1.
    '(5) Read in the note related cards which have
    ' two letters and a colon in column 1.
    '(6) The TM: card is mandatory and must contain
    ' a time value (W,H,Q,E,S) for each note,
    ' with each measure separated by a single or
    ' double bar.  The last time value in the song
    ' must be followed by a double bar (||).
    ' (6B) The last V1 value in the song
    ' must be followed by a double bar (||), in order
    ' to get double bar separators between songs
    ' in the ABC score.
    '(7) For convenience the note related cards
    ' should be arranged in a "system", where
    ' the note related items stack vertically on
    ' the page.
    '(8) In each system, the TM: card must
    ' come first. Then you can
    ' freely insert or omit other types of cards
    ' after them, e.g. RN: or FB: without having
    ' to use all blank cards as placeholders.
    '(9) You must have a Tn: card before a Vn: card.
    ' For convenience in input,
    ' you can use a Tn: Range card, which begins
    ' with, e.g. T2:-T4: in which case the program
    ' will create and process gTextLines with
    ' T2: and T3: and T4:
    ' The Vn: cards can be in any order.
    '(10) If the strong and weak beats of a measure
    ' or system are interspersed with non-harmonic
    ' tone notes (NHT), then the TM: card MUST be
    ' first and must contain a time value for
    ' notes in the smallest division used.
    ' (11) On Tn: and Vn: cards you can insert TAB's
    ' or TAB + hyphens freely to vertically align the
    ' notes.  Only valid notes are processed.
    ' (12) On RN: and FB: cards, you must insert
    ' TAB's or TAB + hyphens to line up the non-blank
    ' fields under the corresponding field on the
    ' TM: card. RN: and FB: go anywhere after the
    ' TM: card.  One or more RN: or FB: cards can be used.
    ' All blank RN: or FB: cards are not needed for
    ' proper spacing.
    
    ' If OPTION
    '   OP: MISC    ABCINPUT    ON
    ' then switch to input of ABCPlus syntax,
    ' as produced by the program
    '   midi2abc
    ' (1) Change K:key in col 1 to KY:key
    ' (2) If a line does not have a colon : in
    ' col 2, then it is a voice line.  If the
    ' last voice line was V:n, then insert
    ' Vn:[TAB] at the beginning of the line.
    ' (3) Convert ABCPlus syntax to 4Part syntax
    ' by changing a V:n line into a Tn: and Vn:
    ' line, calling
    '   sub ConvertAbcToPart. ???
    ' 12/13/6 probably
    '   sub AbcNoteTo4Part
    ' (4) Use Software Tools trick of a pushback
    ' routine to push a changed or new input line
    ' back on an input stack, for the
    '   sub InputLines.

'***************************************

    Dim FileNo As Integer
    Dim OFileNo As Integer
    Dim Prefix As String
    Dim File As String
    Dim OFile As String
    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim bInAbcText As Boolean
    Dim bInSkipText As Boolean
    Dim Dir As String
    Dim E$
    Dim bErr As Boolean
    Dim bSkip As Boolean
    Dim bAbcSongEnd As Boolean
    Dim bAbcSongEndSkip As Boolean
    Dim Voice As Integer
    Dim Voice2 As Integer
    Dim Err As String
    Dim TextLine$
    Dim Tn1 As Integer
    Dim Tn2 As Integer
    Dim SavegbDebugPrint
    Dim A$
    Dim B$
    Dim Note As Integer
    Dim MeasNo As Integer
    Dim NoteNo As Integer
    Dim Field As Integer
    
'***************************************
'***************************************

Main:
    SavegbDebugPrint = gbDebugPrint
    'gbDebugPrint = True
    GoSub Init
    GoSub OpenFile
    GoSub MainLoop
        'fcnbInputLine
            'call ParseAbc
        'GoSub MainSpecialCards
            'GoSub SpecialAbcText ' store ABC hdr cards
            'GoSub SpecialSkipText ' skip 4Part cards
        'GoSub MainSelectCases
            'call StoreSong(gTextLine) ' X: T: M: etc
                'call InitVoiceRangeDefaults
                'call PropagateOptions
            'GoSub TMRangeCard
            'call ParseTabs(gTextLine)
            'call StoreNotes(gTextLine)
    GoSub Conclusion
        'GoSub Debug1
        'GoSub StoreTMedit
        'call FillWithKeys ' propagate gSongKey
    gbDebugPrint = SavegbDebugPrint
    Exit Sub

'***************************************
'***************************************

Init:
    ' following are for cmdNextFile  17aug08
    gAbcHdrLast = 0
    gInputLineNo = 0
    gbRaised = False
    For I = SOPRANO To BASS
        gRaiseInput(I) = 0
    Next I
    gbSeventhExcluded = False
    gbMajorSeventhExcluded = False
    
    ' From sub StoreOptionMisc
    gbSmoothOption = False
    gbRankOverVariance = False
    gbRankLE = False
    gbVarianceLE = False
    'gbRankLT = true
    'gbVarianceLT = true
    gbAbcInputOption = False
    gbNHTInputOption = False
    gMainVoice = 0
    gxMotionRequest = 0
    gbRaise67OneVoice = False
    gbRaise67Close = False
    gbShowInputNotesOnly = False
    gCommentVoices = ""
    gCommentLyrics = ""
    gCommentTimes = ""
    ' from sub StoreSong
    gbAbcInputVoice = False
    gbAbcInputLyric = False
    gbFirstSongBegun = False
    
    ' from sub StoreOptionFigBass
    For I = 1 To FIGBASSALLOWEDMAX
        gbFigBassAllowed(I) = True
    Next I
    MeasNo = 0
    NoteNo = 0
    Return
    
' The following lines are initialized elsewhere
' and are not needed for cmdNextFile  17aug08
    'gMinVarianceForDupes = 2#
   ' gMaxNextBest = MAXNEXTBEST ' default=2
   ' gBassGap = BASSGAPDEFAULT ' 24
   ' gBarBeg = 0
   ' gBarEnd = BARSMAX
   ' gbAbcInputVoice = False
   ' gbAbcInputLyric = False
   ' gAbcInput(I).String = ""
   ' gAbcInput(I).Pos = 0
   ' gbScalesSetup = True
   ' gxErrorAllowed = 0
   ' gxErrorAllowedPass1 = 0 ' allow none of these errors
   ' gxCandErrorBits = 0 ' reset all error bits
   ' gTriadAllowed(I) = True
   ' gNumLyricLines = 0
   ' gFieldLast = 0
    
'***************************************

OpenFile:
    Prefix = gPrefix & gDirSongs
    File = gFile & ".TXT"
    FileNo = FreeFile ' get next available #

    Open Prefix & File For Input As #FileNo
    If g4PartFile <> "" Then
        OFile = g4PartFile & ".TXT"
        OFileNo = FreeFile
        Open Prefix & OFile For Output As #OFileNo
    End If

    If gbDebugPrint Then
        'Debug.Print "InputLines:D2 Opened file="; _
            Prefix & File
    End If
    Return

'***************************************

    ' ASCII values 8, 9, 10 and 13 convert to
    ' backspace, tab, linefeed and carriage return.
MainLoop:
    If gbDebugPrint Then
        'Debug.Print "InputLines:D3 Opened file="; _
            Prefix & File
        Debug.Print "InputLines:D3 begin"
    End If
    Do While (fcnbInputLine(FileNo, gTextLine)) ' call
        If gbDebugPrint Then '27jun08
            Debug.Print "InputLines:D5 "; _
                gInputLineNo; gTextLine
        End If
        If g4PartFile <> "" Then
            A$ = Left$(gTextLine, 3)
            If A$ <> "%m:" _
            And A$ <> "TM:" Then ' See PrintMeasureNos:
                GoSub PrintG4PartFile
            End If
        End If
        GoSub TrimGTextLine
        If Len(gTextLine) = 0 Then
            GoTo MainLoop1
        End If
        GoSub Test1
        bSkip = False
        GoSub MainSpecialCards
        If Not (bSkip Or bErr) Then
            'Debug.Print "InputLines:D6 ";gTextLine
            GoSub MainSelectCases
            If bAbcSongEnd Then ' set in StoreSong
                GoSub Test2
                Call ParseAbc
                GoSub PushAbcLines
                If g4PartFile <> "" Then
                    Call InterleaveAbcLines(OFileNo)
                End If
                bAbcSongEnd = False
                gbAbcInputVoice = False
                gbAbcInputLyric = False
                If g4PartFile <> "" Then
                    Debug.Print "InputLines:D7 ABC->4Part lines"
                End If
            End If
        End If
MainLoop1:
    Loop
    
    If gbDebugPrint Then
        Debug.Print "InputLines:D8 end"
    End If
    Return

'***************************************

' If ABC input option, then after "E:"
' is met, skip any more lines,
' e.g. TM, Tn:. Vn: 1N:,
' till "X:" of a new song is met
' since these are written out by
' sub InterleaveAbcLines in short 4Part
' format lines. 18nov08

PrintG4PartFile:
    If Not gbAbcInputOption Then
        Print #OFileNo, gTextLine
        Return
    End If
    
    If bAbcSongEndSkip = False Then
        Print #OFileNo, gTextLine
        If Left$(gTextLine, 2) = "E:" Then
            bAbcSongEndSkip = True
        End If
    Else
        If Left$(gTextLine, 2) = "X:" Then
            Print #OFileNo, gTextLine
            bAbcSongEndSkip = False
        End If
    End If
    Return

'***************************************

' Trim blanks, then
' drop any trailing "\", the ABC
' continuation char, and trim blanks
' again, usually to a trainlng "|".

TrimGTextLine:
      gTextLine = Trim$(gTextLine)
      If Right$(gTextLine, 1) = "\" Then
          gTextLine = Trim$(Left$(gTextLine, _
              Len(gTextLine) - 1))
      End If
      Return

'***************************************

Test1:
    If gTextLine = "V:1" Then
        I = I ' testing
    End If
    If gTextLine = "X:2" Then
        I = I ' testing
    End If
    If gTextLine = "E:" Then
        I = I ' testing
    End If
    If gTextLine = "E:." Then
        I = I ' testing
    End If
    Return

'***************************************

Test2:
    If gTextLine = "E:" Then
        I = I ' testing
    End If
    If gTextLine = "E:." Then
        I = I ' testing
    End If
    Return

'***************************************

' NOT USED 23aug08
        'If InStr(gTextLine, "INCLUDE") > 0 _
        'Or InStr(gTextLine, "EXCLUDE") > 0 Then
            'Debug.Print gTextLine
            'I = I ' testing
        'End If

'***************************************

MainSpecialCards:
    If bInAbcText Then
        GoSub SpecialAbcText ' store ABC cards
        bSkip = True

    ElseIf bInSkipText Then
        GoSub SpecialSkipText ' skip 4Part cards
        bSkip = True

    ElseIf Left$(gTextLine, 10) = "%ABC BEGIN" _
        Then
        bInAbcText = True
        bSkip = True

    ElseIf Left$(gTextLine, 11) = "%SKIP BEGIN" _
        Then
        bInSkipText = True
        bSkip = True
    Else
        ' Bypass comment lines with % or blank in col 1
        'Or Left$(gTextLine, 1) = " " ' 22aug08
        If Left$(gTextLine, 1) = "%" _
        Or gTextLine = " " _
        Or Len(gTextLine) = 0 Then
            bSkip = True
        Else  ' strip off comment after col 1 beg. with %
            I = InStr(gTextLine, "%")
            If I > 1 Then
                gTextLineFull = gTextLine
                If Mid$(gTextLine, I - 1, 1) = Chr$(9) Then ' TAB
                    gTextLine = Trim$(Left$(gTextLine, I - 2))
                Else
                    gTextLine = Trim$(Left$(gTextLine, I - 1))
                End If
            Else
                gTextLineFull = ""
            End If
        End If
    End If
    
    Return

'***************************************

SpecialAbcText:
    If Left$(gTextLine, 8) = "%ABC END" Then
        bInAbcText = False
    Else
        If gAbcHdrLast < ABCHDRMAX Then
            gAbcHdrLast = gAbcHdrLast + 1
            gAbcHdr(gAbcHdrLast) = gTextLine
        Else
            bErr = True
            GoSub Err5 ' err msg, but keep reading
        End If
    End If
    Return

'***************************************

SpecialSkipText:
    If Left$(gTextLine, 9) = "%SKIP END" Then
        bInSkipText = False
    End If
    Return

'***************************************

' Notice the different use of (ABCInput) V:1
' and (standard input) V1:

' Notice the different use of (ABCInput) i:
' and (standard input) 11:

' If ":" in col 2
' I: ABCInputLyric line to be concatenated
' V: or no ":" is ABCInput line to be concatenated
' E: end of ABCInput after all voices V:n
' Song controls:
' X: T: M: L: Q: V: E:

' If ":" in col 3
' OP: MISC and other OP: line controls  or

' Note line controls:
' TM: T1: T2: T3: T4:   or  T1:-T4:
' V1: V2: V3: V4:  or
' 11: 21: 31: 41:  one lyric line for each voice
' 11: 12: 13: 14:  four lyric lines for one voice

' Else, if no ":" in cols 1-3, then
' concatenate the gTextLine to the
' gAbcInput(n).string for the last
' V:n found.

MainSelectCases:
    I = InStr(gTextLine, ":")
    Select Case I ' col pos of colon
    Case 2
        If UCase$(Left$(gTextLine, 2)) = "I:" Then
            GoSub ConcatenateLyric ' i: my lyric ...
        Else
            GoSub StoreSong1 ' X: T: M: L: Q: V: E:
        End If
    Case 3
        If Mid$(gTextLine, I, 3) = ":-T" Then
            GoSub TMRangeCard
        Else
            If Len(gTextLine) > 3 Then
                ' parse the TAB fields into gField(n)
                Call ParseTabs(gTextLine)
                ' e.g. T1: V1: 11: OP: etc
                GoSub PrintMeasureNos
                Call StoreNotes(gTextLine)
            End If
        End If
    Case 0
        GoSub ConcatenateVoice
    Case Else ' colon is past col 3
        GoSub Debug10
    End Select
    Return
    
'***************************************
' Recalculate and print a line before
' the TM: line, containing measure numbers
' and note numbers.
' The same lines printed when sub ParseNote
' converts ABC input lines into 4Part
' format lines will often be modified when
' harmonizing, testing and editing the music.

PrintMeasureNos:
    If Left$(gTextLine, 3) <> "TM:" Then
        Return
    End If
    If Not gbDebugPrint _
    And g4PartFile = "" Then
        Return
    End If
    
    TextLine$ = gTextLine ' save TM: line
    MeasNo = MeasNo + 1
    gTextLine = "%m:" & Format(MeasNo)
    For Field = 1 To gFieldLast - 1
        A$ = gField(Field)
        If A$ = "|" Or A$ = "||" Then
            MeasNo = MeasNo + 1
            gTextLine = gTextLine & Chr$(9) _
                & "| m:" & Format(MeasNo)
            NoteNo = NoteNo + 1
        Else
            NoteNo = NoteNo + 1
            gTextLine = gTextLine & Chr$(9) _
                & Format(NoteNo)
        End If
    Next Field
    
    NoteNo = NoteNo + 1
    gTextLine = gTextLine & Chr$(9) & "|"
    If gbDebugPrint Then '27jun08
        Debug.Print "InputLines:D5 "; _
            gInputLineNo; gTextLine
    End If
    If g4PartFile <> "" Then
        GoSub PrintG4PartFile
    End If
    gTextLine = TextLine$ ' TM: etc
    GoSub PrintG4PartFile
    Return
        
    
'***************************************

StoreSong1:
     'sub StoreSong sets gSongLast=gSongLast+1
    Call StoreSong(gTextLine, bAbcSongEnd)
    ' X: T: M: L: Q: V: E:
    ' only do this once per song
    If Left$(gTextLine, 1) = "X" Then
        For I = SOPRANO To BASS
            gAbcInput(I).String = ""
            gAbcInput(I).Pos = 0
            gAbcInput(I).bUsed = False
            gAbcInputLyric(I).String = ""
            gAbcInputLyric(I).Pos = 1 ' see
            ' ParseAbcNoteName GetLyricForThisNote:
            gAbcInputLyric(I).bUsed = False
        Next I
        If gSongLast <= 1 Then
            Call InitVoiceRangeDefaults
        Else ' gSongLast > 1
            Call PropagateOptions ' after gSongLast set
        End If
    End If
    Return

'***************************************

' assumes no colon in ABC language text
' Concatenate the abc note data,
' already trimmed of "\" and blanks,
'     " " & gTextLine
' for this voice n
' following the last V:n card met.
' n = gAbcVoice is updated in Sub StoreSong.
' There is no practical limit to the length
' of a string.

ConcatenateVoice:
    If gbAbcInputOption Then ' OP: MISC ABCINPUT ON
       ' count the number of single or double bars.
        K = fcnCountChar(gTextLine, "|")
        K = K - fcnCountChar(gTextLine, "||")
        If K > 0 Then
            K = K
        End If
        If gBarsVoice(gAbcVoice) >= gBarBeg - 1 _
        And gBarsVoice(gAbcVoice) <= gBarEnd - 1 Then
            gAbcInput(gAbcVoice).String = _
                gAbcInput(gAbcVoice).String & _
                " " & gTextLine
            gAbcInput(gAbcVoice).bUsed = True
            gbAbcInputVoice = True
        End If
        gBarsVoice(gAbcVoice) = _
            gBarsVoice(gAbcVoice) + K
    ElseIf Len(gTextLine) > 0 Then
        GoSub Err1
    End If
    Return

'***************************************

' The i: lyric lines must follow it's
' voice line which does not have a
' leading V:n, since this is doing
'     gbAbcInputOption = True
' A leading V:n needs to come first, which
' sets the voice for gAbcVoice.

' Concatenate the abc lyric note data,
' minus the leading "i:",
' already trimmed of trailing "\" and blanks,
'     Mid$(gTextLine, 3)) & " "
' for this voice n
' following the last V:n card met.

' Each lyric word must end with a blank
' for the logic updating
'     gAbcInputLyric(gAbcVoice).Pos
' at label GetLyricForThisNote: in sub
'     ParseAbcNoteBar
' so add a blank after each gTextLine.

' gBarBeg and gBarEnd are set by
'     OP:   MISC  BarBeg      0
'     OP:   MISC  BarEnd      20
' defaults are 0 to BARSMAX = 300.

ConcatenateLyric:
    If gbAbcInputOption Then ' OP: MISC ABCINPUT ON
        GoSub CheckForTrailingBar
        GoSub SpaceBeforeUnderScore
       ' count the number of single or double bars.
        K = fcnCountChar(gTextLine, "|")
        K = K - fcnCountChar(gTextLine, "||")
        If K > 0 Then
            K = K
        End If
        If gBarsLyric(gAbcVoice) >= gBarBeg - 1 _
        And gBarsLyric(gAbcVoice) <= gBarEnd - 1 Then
            gAbcInputLyric(gAbcVoice).String = _
                gAbcInputLyric(gAbcVoice).String & _
                Trim$(Mid$(gTextLine, 3)) & " "
            gAbcInputLyric(gAbcVoice).bUsed = True
            gbAbcInputLyric = True
        End If
        gBarsLyric(gAbcVoice) = _
            gBarsLyric(gAbcVoice) + K
    ElseIf Len(gTextLine) > 0 Then
        GoSub Err1
    End If
    Return
    
'***************************************

' There should be a " " & bar at the end of
' every measure.
' Add " |" if missing at the end of any
' lyric line to be concatenated. 19sep08
' See label GetLyricForThisNote: in sub
'     ParseAbcNoteBar
' Else, replace any bar with " |".

CheckForTrailingBar:
    If Right(gTextLine, 1) <> "|" Then
        gTextLine = gTextLine & " |"
    Else
        gTextLine = fcnReplace(gTextLine, _
              "|", " |")
    End If
    Return
    
'***************************************

' Every underscore "_" in a lyric line should
' have a blank before it. 19sep08

' WARNING - this may result in two blanks
' before a "_". 20sep08

SpaceBeforeUnderScore:
      If InStr(gTextLine, "_") Then
            I = I
      End If
      A$ = fcnReplace(gTextLine, "_", " _")
      'A$ = fcnReplace(A$, "  ", " ") ' reduce to 1 blank
      A$ = fcnRepDupe(A$, " ") ' reduce to 1 blank
      gTextLine = A$
      Return
      
      
SpaceBeforeUnderScore9:
    K = 0 ' loop insurance
    I = InStr(gTextLine, "_")
    Do While I > 0
        K = K + 1
        If K > 3 Then
            K = K
        End If
        If Mid$(gTextLine, I - 1, 1) <> " " Then
            A$ = Left$(gTextLine, I - 1) _
                & " " & Mid$(gTextLine, I)
            gTextLine = A$
            I = I + 1 ' re-point to "_"
        End If
        I = I + 1 ' one byte past "_"
        If I <= Len(gTextLine) Then
            J = InStr(Mid$(gTextLine, I), "_")
            If J > 0 Then
                I = I + J - 1
            Else
                I = 0
            End If
        Else
            I = 0
        End If
        'Debug.Print "I, J, len(a$);A$="; _
            I; J; Len(A$); A$
    Loop
    Return:
    
'***************************************

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "InputLines:D9 LineNo="; _
        gInputLineNo; "missing ':' in "; _
        gTextLine
    I = I
    Stop
    Return

'***************************************

' If gTextLine starts with a range of Tn:'s,
' e.g. T2:-T4:, then create and process a
' Tn: card for each number in the range.
' This is only for convenience in input.
TMRangeCard:
    TextLine$ = gTextLine
    Tn1 = Val(Mid$(gTextLine, 2))
    Tn2 = Val(Mid$(gTextLine, 6))
    GoSub TMRangeCardEdit
    For I = Tn1 To Tn2
        gTextLine = "T" & Format(I) & ":" & Mid$(TextLine$, 8)
        Call ParseTabs(gTextLine)
        Call StoreNotes(gTextLine) ' V1: etc
    Next I
    Return
    
'***************************************
TMRangeCardEdit:
    If Mid$(TextLine$, 7, 1) <> ":" Then
        GoSub Err6
    ElseIf Tn1 < 1 Or Tn1 > 4 Then
        GoSub Err6
    ElseIf Tn2 < 1 Or Tn2 > 4 Then
        GoSub Err6
    End If
    If bErr Then
        Stop
    End If
    Return

'***************************************

' Push back the ABC lines that were
' converted to 4Part format on the input
' line stack in reverse order.
' Sub InputLines will then retrieve them in
' this order:
'   NoteNo  MeasNo
'   TM:  T1: V1:  T2: V2:  T3: V3:  T4: V4:
'   1I:   2I:   3I:   4I:
' MAXINPUTSTACK = 15 = 9 + 2 + 4

PushAbcLines:
    If Len(gAbcT(BASS).String) = 0 Then
        'gAbcT(BASS).String = "T4:" & _
        Mid$(gAbcTM.String, 4) ' T4: is needed for Bass4
    End If
    
    For Voice = BASS To SOPRANO Step -1
        If Len(gAbcI(Voice).String) > 0 Then
            Call PushBackLine(gAbcI(Voice).String, Err)
            If Err <> "" Then Stop
        End If
    Next Voice
    
    For Voice = BASS To SOPRANO Step -1
        If Len(gAbcV(Voice).String) > 0 Then
            Call PushBackLine(gAbcV(Voice).String, Err)
            If Err <> "" Then Stop
        End If
        If Len(gAbcT(Voice).String) > 0 Then
            Call PushBackLine(gAbcT(Voice).String, Err)
            If Err <> "" Then Stop
        End If
    Next Voice
    Call PushBackLine(gAbcTM.String, Err)
    If Err <> "" Then Stop
    Call PushBackLine(gAbcNoteNo.String, Err)
    If Err <> "" Then Stop
    Call PushBackLine(gAbcMeasNo.String, Err)
    If Err <> "" Then Stop
    Return
    
'***************************************

Conclusion:
    Close #FileNo    ' Close file.
    If g4PartFile <> "" Then
        Close #OFileNo    ' Close file.
    End If
    GoSub Debug1
    If gSongLast < 1 Then
        gInputErrs = gInputErrs + 1
        Debug.Print "InputLines:D11 no songs were"; _
            " input."
    Else
        If gSong(gSongLast).NumDoubleBars < 1 Then
            gInputErrs = gInputErrs + 1
            Debug.Print "InputLines:D12 missing double"; _
                " bar in a TM: line in the last song."
        Else
            Call StoreTMedit(bErr)
            If Not bErr Then
                Call FillWithKeys ' propagate gSongKey
                ' for the last song input.
                If gbNHTInputOption Then
                    Call FlagNHTHang
                End If
                'GoSub CombineVoices1and4 ' only for Credo 2dec08
            End If
        End If
    End If
    Return

'***************************************

' Combine voices 1 and 4:
' Take tones from V4 if not = "z", else from V1.
' If V4 is valid, then null out V1's tone.
' Null out V2 and V3 in any case.
CombineVoices1and4:
    For Note = gSong(gSongLast).FirstNote To _
                gSong(gSongLast).LastNote
        With gNote(Note)
            If Note = 40 Then
                Note = Note
            End If
            'GoSub checkSlurTie ' testing only
            GoSub CombineVoices
            'GoSub SuppressVoices
        End With
    Next Note
    Return
    
'***************************************

checkSlurTie:
    With gNote(Note)
        For Voice = SOPRANO To BASS
            B$ = .Voice(Voice).Name
            If B$ <> "" Then
                If InStr(B$, "(") _
                Or InStr(B$, ")") _
                Or InStr(B$, "-") Then
                    I = I
                End If
            End If
        Next Voice
    End With
    Return
        
    Return
    
'***************************************

CombineVoices:
    With gNote(Note)
        If Not .bRestAll4 Then
            If .Voice(SOPRANO).Name = "z" Then
                .Voice(SOPRANO).Name = ""
                .Voice(SOPRANO).Index2Name = 0
            End If
        End If
        If Not .bRestAll4 Then
            If .Voice(BASS).Name = "z" Then
                .Voice(BASS).Name = ""
                .Voice(BASS).Index2Name = 0
                If .Voice(SOPRANO).Index2Name <= 0 Then
                    Note = Note ' "-1" means "z"
                End If
            End If
        End If
        
        ' Keep the Soprano as the melody line,
        ' and suppress the other three voices.
        ' Where the Soprano line has blank notes,
        ' then keep the bass line
        ' and suppress the other three voices.
        
        ' This code works only for Part's Credo,
        ' which has two voices blank at times. 29oct08
        
        If .Voice(SOPRANO).Index2Name > 0 _
        And .Voice(ALTO).Index2Name > 0 Then
            Voice = ALTO
            GoSub KeepSlursAndTies
            Voice = TENOR
            GoSub KeepSlursAndTies
            Voice = BASS
            GoSub KeepSlursAndTies
        ElseIf .Voice(BASS).Index2Name > 0 _
        And .Voice(TENOR).Index2Name > 0 Then
            Voice = SOPRANO
            GoSub KeepSlursAndTies
            Voice = ALTO
            GoSub KeepSlursAndTies
            Voice = TENOR
            GoSub KeepSlursAndTies
        End If
        
        ' If any voice has a minus offset, then
        ' this a a NHT chord, so blank out all
        ' tones for this song: Arvo's Credo. 29oct08
        For Voice2 = SOPRANO To BASS
            If .Voice(Voice2).Time.Offset < 0 Then
                Voice = SOPRANO
                GoSub KeepSlursAndTies
                Voice = ALTO
                GoSub KeepSlursAndTies
                Voice = TENOR
                GoSub KeepSlursAndTies
                Voice = BASS
                GoSub KeepSlursAndTies
                Exit For
            End If
        Next Voice2
    
    End With
    Return
    
'***************************************

' Keep only slurs and ties: ( ) - ' 22oct08
KeepSlursAndTies:
    With gNote(Note).Voice(Voice)
        B$ = ""
        For I = 1 To Len(.Name)
            A$ = Mid$(.Name, I, 1)
            If A$ = "(" _
            Or A$ = ")" _
            Or A$ = "-" Then
                B$ = B$ & A$
            End If
        Next I
    .Name = B$
    .Index2Name = 0
    End With
    Return
    
'***************************************

SuppressVoices:
    With gNote(Note)
        '.Voice(SOPRANO).Name = ""
        '.Voice(SOPRANO).Index2Name = 0
        .Voice(ALTO).Name = ""
        .Voice(ALTO).Index2Name = 0
        .Voice(TENOR).Name = ""
        .Voice(TENOR).Index2Name = 0
        '.Voice(TENOR).lyric(1) = ""
        '.Voice(BASS).Name = ""
        '.Voice(BASS).Index2Name = 0
    End With
    Return

'***************************************

Debug1:
    'If gbDebugPrint Then
    If Not gbDebugPrint Then ' 18jul08
        E$ = "InputLines:D1 Closed file=" & _
            Prefix & File
        Call DisplayText(E$)
        Debug.Print E$
        E$ = "InputLines:D1 # input lines=" _
            & gInputLineNo
        Call DisplayText(E$)
        Debug.Print E$
    End If
    Return

'***************************************

Err5:
    gInputErrs = gInputErrs + 1
    E$ = "InputLines:E5 reached ABC Hdr max " & _
        "lines=" & _
        ABCHDRMAX & " in song no" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    I = I
    Stop
    'Exit Sub
    Return

'***************************************

Err6:
    bErr = True
    gInputErrs = gInputErrs + 1
    E$ = "InputLines:E6 invalid Tn: range card=" & _
        gTextLine & " should start like T2:-T4: " & _
        "in song=" & gSongLast & _
        "at LineNo=" & gInputLineNo
    Debug.Print E$
    Call DisplayText(E$)
    Return

'***************************************

Debug10:
        gInputErrs = gInputErrs + 1
        Debug.Print "InputLines:D10 LineNo="; _
            gInputLineNo; "extra ':' in "; _
            gTextLine
        Return

End Sub

Private Sub StoreSong(gTextLine As String, _
                pbAbcSongEnd As Boolean)
    ' called by sub InputLines

    ' calls sub PushBackLine(gTextLine, Err$)
    ' calls sub FillWithKeys
    ' calls function fcnPack

    ' Parse a whole input line, and store the _
    ' values.  Either the whole line is stored,
    ' or all the fields are separated by a tab
    ' character, chr(9).

    ' Process Input Lines:
    '   X: n
    '   T: text...
    '   M: 4/4
    '   L: 1/8 @WN=32U where  @WN=32U is needed
    '        only the 1st time before or in a song.
    '   Q:1/4=90
    '   V:n where n=1-4 in for gAbcInput=True

    'ElseIf InStr$(1, gTextLine, Chr$(9)) > 0 Then
    'Debug.Print gTextLine    ' Print to Debug window.

    ' TODO: increment song numbers internally,
    ' and allow any song number n in any order
    ' on the X:n line, if n is not a duplicate.
    ' gNoteLast = 0 initially so that first
    ' note used = 1.
    ' gsonglast=1 initially so that first
    ' song used = 1.

    Dim RecType As String * 1
    Dim I As Integer
    Dim J As Integer
    Dim A$
    Dim B$
    Dim E$
    Dim Err$
    Dim bErr As Boolean

'***************************************
'***************************************

Main:
    GoSub EditIdent
    If bErr Then
        Exit Sub
    End If
    GoSub RecordTypes
        'call PushBackLine(gTextLine, Err$)
    Exit Sub

'***************************************
'***************************************

' Not < 3 chars.  Trim off blanks.
EditIdent:
    bErr = False
    If Len(gTextLine) < 3 _
    And gTextLine <> "E:" Then
        gInputErrs = gInputErrs + 1
        Debug.Print "StoreSong:D1 LineNo="; _
            gInputLineNo; "invalid length="; _
            Len(gTextLine); " in "; _
            gTextLine
        bErr = True
    Else
        A$ = Trim$(Mid$(gTextLine, 3)) ' rest of
        ' line after the X: which should be n
    End If
    Return

'***************************************

' gbAbcInputVoice is set and used in this sub
' StoreSong and in function fcnbInputLine.
' It enables end of song processing using
'   pbAbcSongEnd.
' The period in "E:." is dummy.
RecordTypes:
    RecType = UCase$(Left$(gTextLine, 1))
    If RecType = "E" Then ' end of this song,
        ' Set by EOF or below at the next X:n by:
        '   call PushBackLine("E:."
        If gbAbcInputOption Then
            If gbAbcInputVoice Then
                gbAbcInputVoice = False
                gbAbcInputLyric = False
                pbAbcSongEnd = True
            End If
        End If
    ElseIf RecType = "X" Then ' initialize new song
        If gbAbcInputOption And gbAbcInputVoice Then
            gbAbcInputVoice = False
            gbAbcInputLyric = False
            pbAbcSongEnd = True
            Call PushBackLine(gTextLine, Err$)
            If Err$ <> "" Then
                Stop
                I = I ' testing
            End If
            'call PushBackLine("E:.", Err$)
            'If Err$ <> "" Then
            '    Stop
            'End If
        Else
            GoSub EndofPrevSong
                'GoSub Err1
                'call FillWithKeys ' propagate gSongKey
            If Not bErr Then
                gbAbcInputVoice = False ' 29aug08
                gbAbcInputLyric = False
                pbAbcSongEnd = False
                GoSub NewSong
                    'GoSub BumpSong
                    'GoSub ResetNotePos
            End If
        End If
    Else
        GoSub OtherParms
    End If
    Return

'***************************************

' Skip this if the beginning of the first song.
' Do this at the end of a song to propagate
' gSongKey to every note's .SongKey that
' was not already set by a modulation key
' Kn: on a RN: card.
EndofPrevSong:
    If gbFirstSongBegun Then
        If gSong(gSongLast).NumDoubleBars < 1 Then
            bErr = True
            GoSub Err1
        Else
            Call FillWithKeys ' propagate gSongKey
            ' TODO cleanup
            'gSong(gSongLast).BASSGAP = gBassGap
            If gbNHTInputOption Then
                Call FlagNHTHang
            End If
        End If
    Else
        gbFirstSongBegun = True
    End If
    Return

'***************************************

NewSong:
    'gSong(gSongLast).NumDoubleBars = 0
    I = Val(A$) ' X: n
    If I > 0 Then
        ' gbAbcInputVoice = True
        ' gbAbcInputLyric = True
        ' gSongLast=1 initially to handle OP: cards
        If gSongLast < I Then
            GoSub BumpSong
        End If
        With gSong(gSongLast)
            .X = Str$(I)
            ' first note used is gNote(1)
            .FirstNote = gNoteLast
            ' initialize to not yet used:
            ' TODO init to 0?
            .LastNote = .FirstNote - 1
            For J = 1 To LASTUSEDMAX
                .LastUsed(J) = .FirstNote - 1
            Next J
        End With
        GoSub ResetNotePos
        'Exit Sub
        gNumLyricLines = 0
        gSong(gSongLast).xMotionRequest = _
            gxMotionRequest
        For I = 0 To FIGBASSALLOWEDMAX
            gSong(gSongLast).bFigBassAllowed(I) = _
                gbFigBassAllowed(I) ' 11may08
            'gSong(gSongLast).bFigBassAllowed(I) = _
            '    gSong(gSongLast - 1).bFigBassAllowed(I)
        Next I
  End If
    Return

'***************************************

ResetNotePos:
    For I = TM To T4 ' 1 to 5
        gNotePos(I) = 0 ' reset to beg of measure
    Next I
    Return

'***************************************

BumpSong:
        If gNoteLast >= SONGNOTEMAX Then
            Debug.Print "StoreSong:D2 "; _
                "Reached NOTEMAX="; SONGNOTEMAX
            Stop
        Else
            gNoteLast = gNoteLast + 1
        End If

        If gSongLast >= SONGMAX Then
            Debug.Print "StoreSong:D3 "; _
                "Reached SONGMAX="; SONGMAX
            Stop
        Else
            gSongLast = gSongLast + 1
            ' re-initialize with gSong(0) for cmdNextFile
            If gbFileNameNext Then
                If gSong(gSongLast).bUsed Then
                    gSong(gSongLast) = gSong(0)
                End If
            End If
            gSong(gSongLast).bUsed = True
        End If
        Return

'***************************************

OtherParms:
    With gSong(gSongLast)
        Select Case RecType
        Case "K"
            GoSub ParmK
        Case "T"
            .T = A$
            txtDesc.Text = .T
        Case "M"
            GoSub ParmM
        Case "L"
            GoSub ParmL
        Case "Q"
            GoSub ParmQ
        Case "V"
            GoSub ParmV
        Case Else
            gInputErrs = gInputErrs + 1
            Debug.Print "StoreSong:D4 LineNo="; _
                gInputLineNo; "col.1 invalid in "; _
                gTextLine
        End Select
    End With
    Return

'***************************************

ParmK:
    ' Bypass: take KY: K1: from 4Part input
    ' before the ABC input.
    ' See NOT USED 1
    GoSub GetKeyName ' 17jul08
    If gbAbcInputOption Then
        ' Reload the 4Part .Key data from loc=0
        ' for use at ABC format K:c card in
        ' sub StoreSong.  Saved in sub StoreNotes
        If gSongLast > 0 Then
            For I = 1 To NUMKEYMAX
               gSong(gSongLast).Key(I) = _
                   gSong(0).Key(I)
               gSong(gSongLast).Key(I).Name = _
                   gSong(0).Key(I).Name
               gSong(gSongLast).Key(I).Mode = _
                   gSong(0).Key(I).Mode
            Next I
        End If
       'GoSub GetKeyName
    Return

'***************************************

GetKeyName:
        With gSong(gSongLast)
            ' KeyNo = 1 should be input first:
            ' Taken from sub StoreNotes
            If .Key(1).Name = "" Then
                bErr = True
                'GoSub Err3
                Stop
                I = I ' testing
            Else
                ' convert fixed string*2 to string:
                gKeyName = Trim$ _
                    (.Key(1).Name & " ")
                gKeyMode = .Key(1).Mode
                'GoSub PropagateParms
            End If
        End With
     End If
     Return

'***************************************

ParmM: ' M: 4/4
    With gSong(gSongLast)
        '.M = Val(A$)
        .M.Text = fcnPack(A$)
        txtMeter.Text = A$
        .M.Numer = Val(A$)
        If .M.Numer <= 0 Then
            GoSub Err2
            GoTo ReturnM
        End If
        I = InStr(A$, "/")
        If I > 0 Then
            .M.Denom = Val(Mid$(A$, I + 1))
            If .M.Denom <= 0 Then
                GoSub Err2
                GoTo ReturnM
            End If
        Else
            GoSub Err2
            GoTo ReturnM
        End If
        ' Get total 1/32 units per measure
        If .M.Denom > 0 Then
            .M.MeasUnits = .L.WNUnits * _
                .M.Numer / .M.Denom
            .M.BeatUnits = .L.WNUnits / .M.Denom
        End If
    End With
ReturnM:
    Return

'***************************************
 ' E.g. L: 1/8 @WN=32U
 ' where WN=32U is only needed the 1st time
ParmL:
    With gSong(gSongLast)
        I = InStr(A$, "@WN=")
        If I = 0 Then
            If gWNUnitsText = "" Then ' needed 1st time
                GoSub Err4
                A$ = A$ & "@WN=32U" ' give a default
                I = InStr(A$, "@WN=")
            Else
                A$ = A$ & gWNUnitsText
                I = InStr(A$, "@WN=")
            End If
        End If
        If I = 0 Then
            .L.Text = A$ ' 1/8
        Else
            .L.Text = Trim(Left$(A$, I - 1)) ' 1/8
            gWNUnitsText = Mid$(A$, I) '  @WN=32U
            .L.UText = gWNUnitsText
            .L.WNUnits = Val(Mid$(A$, I + 4)) ' 32U
            If .L.WNUnits <= 0 Then
                GoSub Err3
                GoTo ReturnL
            Else
                .L.TimeUnit = 1# / .L.WNUnits _
                    ' string variable
                ' Get total 1/32 units per measure
                If .M.Denom > 0 Then
                    .M.MeasUnits = .L.WNUnits * _
                        .M.Numer / .M.Denom
                    .M.BeatUnits = _
                        .L.WNUnits / .M.Denom
                End If
            End If
        End If
        'txtLength.Text = .L.Text ' 24jul08

        ' Eg. if L: 1/8, then .L.Default="E"
        I = InStr(DEFAULTTIMES, " " & .L.Text & " ")
        If I > 0 Then
            .L.Default = Mid$(DEFAULTTIMES, I - 1, 1)
        Else
            GoSub Err3
            GoTo ReturnL
        End If

        .L.Numer = Val(.L.Text) ' eg =1 of 1/8
        If .L.Numer <> 1 Then
            GoSub Err3
            GoTo ReturnL
        End If

        I = InStr(.L.Text, "/")
        If I = 0 Then
            GoSub Err3
            GoTo ReturnL
        Else
            ' eg =8 of 1/8
            .L.Denom = Val(Mid$(.L.Text, I + 1))
            If .L.Denom <= 0 Then
                GoSub Err3
                GoTo ReturnL
            Else
                .L.LUnits = .L.WNUnits / .L.Denom
            End If
        End If
    End With
ReturnL:
    Return

'***************************************

    ' The format of the ABCPlus tempo command
    ' should be like "Q:1/4=90".  Any blanks
    ' are stripped out by fcnPack.
ParmQ:
    B$ = fcnPack(A$) ' strip out blanks
    txtTempo.Text = B$
    gSong(gSongLast).QText = B$
    ' Minimal edit.  Must have / followed by =.
    I = InStr(B$, "/")
    If I = 0 Then
        GoSub Err5
    Else
        I = InStr(Mid$(B$, I + 1), "=")
        If I = 0 Then
            GoSub Err5
        End If
    End If
    Return

'***************************************

    ' Save this voice number and start
    ' concatenating its midi2abc note data
    ' Normally, the output of midi2abc is
    ' just V:n.  But in case there is more note
    ' data, concatenate the rest of the line.
    
    ' If this voice I in V:I is found in
    ' gCommentVoices then comment out this
    ' input ABCInput voice by adding "%" to the
    ' beginning of gAbcInput(gAbcVoice).String.
    ' 28sep08
ParmV:
    With gSong(gSongLast)
        I = Val(A$)
        If I >= SOPRANO And I <= BASS Then
            gAbcVoice = I
            gAbcInput(gAbcVoice).String = _
                gAbcInput(gAbcVoice).String & _
                Trim$(Mid$(gTextLine, 4))
            'If Len(gAbcInput(gAbcVoice).String) > 3 Then
            If Len(gTextLine) > 3 Then
                gAbcInput(gAbcVoice).bUsed = True
            End If
            'GoSub CheckCommentVoices ' 29sep08
        Else
            gInputErrs = gInputErrs + 1
            Debug.Print "StoreSong:D6 LineNo="; _
                gInputLineNo; "ABC V:n invalid in "; _
                gTextLine
        End If
    End With
    Return

'***************************************

CheckCommentVoices:
    B$ = Left$(gTextLine, 3)
    If InStr(gCommentVoices, B$) > 0 Then
        gAbcInput(gAbcVoice).String = "%" _
            & gAbcInput(gAbcVoice).String
    End If
    Return

'***************************************

Err1:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreSong:E1 begun new song="; A$; _
        " but missing double measure bars"; _
        " in a TM: line in the previous song. "
    I = I
    Return

'***************************************

Err2:
    bErr = True
    gInputErrs = gInputErrs + 1
    E$ = "StoreSong:E2 invalid M: card=" & A$ & _
        " should be like M: 4/4 " & _
        "in song=" & gSongLast & _
        "at LineNo=" & _
        gInputLineNo & _
        "in line=" & gTextLine
    Debug.Print E$
    Call DisplayText(E$)
    Return

'***************************************

Err3:
    bErr = True
    gInputErrs = gInputErrs + 1
    E$ = "StoreSong:E3 invalid L: card=" & A$ & _
        " should be like L: 1/8 or L: 1/8 @WN=32U " & _
        "in song=" & gSongLast & _
        "at LineNo=" & _
        gInputLineNo & _
        "in line=" & gTextLine
    Debug.Print E$
    Call DisplayText(E$)
    Return

'***************************************

Err4:
    'bErr = True
    'gInputErrs = gInputErrs + 1
    E$ = "StoreSong:E4 invalid first L: card=" & A$ & _
        " should be like L: 1/8 @WN=32U " & _
        "in song=" & gSongLast & _
        "at LineNo=" & _
        gInputLineNo & _
        "in line=" & gTextLine
    Debug.Print E$
    Call DisplayText(E$)
    Return

'***************************************

Err5:
    bErr = True
    gInputErrs = gInputErrs + 1
    E$ = "StoreSong:E5 invalid Q: card=" & A$ & _
        " should be like Q:1/4=90 " & _
        "in song=" & gSongLast & _
        "at LineNo=" & _
        gInputLineNo & _
        "in line=" & gTextLine
    Debug.Print E$
    Call DisplayText(E$)
    Return

End Sub
Private Sub FillWithKeys()
    ' called by sub InputLines
    ' called by sub StoreSong

    ' Globals used:
    ' Globals set:

    ' Do this at the end of a song to propagate
    ' gSongKey to every note's .SongKey that
    ' was not already set by a modulation key
    ' Kn: on a RN: card.

    ' At the end of all input cards, do this to
    ' propagate gSongKey for the last song input.

    ' (1) If a new modulation key Kn: occurs on an
    ' RN: card, say K2: later followed by K1:,
    ' the the StoreRM procedure stores that "2"
    ' or "1" in gNote().SongKey.

    ' If this procedure finds a non-zero in
    ' gNote(Note).SongKey, then it propagates that
    ' value to all following notes until the
    ' next modulation key, say K1:, is found.

    ' The following is found in sub StoreNotes:
    '       gSongKey = Note
    ' propagate to all notes in the song until
    ' a new modulation key is entered.

    ' (2) Set the Beat number in gSong().Beat if
    ' the position of the note in the measure
    ' is a multiple of
    '   gSong().MeasTime.BeatUnits
    ' e.g. if the time signature is 3/4
    ' and if there are 32 units per whole note,
    ' then the .BeatUnits = 32 / 4 = 8.
    ' At note positions 0, 8, 16 in
    '   gNote().MeasTime.Pos
    ' set the beat as 1, 2, 3 in
    '   gNote().Beat
    ' This is used in Sub Harmonize.
    
    Dim I As Integer
    Dim N As Integer
    Dim Note As Integer
    Dim BeatUnits As Integer
    
    BeatUnits = gSong(gSongLast).M.BeatUnits
    
    For Note = gSong(gSongLast).FirstNote _
        To gSong(gSongLast).LastNote
        If Left$(gNote(Note).TMName, 1) <> "|" Then
            GoSub PropagateSongKey
            GoSub SetTheBeat
        End If
    Next Note
    Exit Sub
    
'**************************************
' TODO 16jul08 check what happens when the keys
' in a song alternate F, C, F where KY: F, K2: C.
' Probably OK since K1: F is used to switch back.
PropagateSongKey:
    If gNote(Note).SongKey = 0 Then
        If Note = gSong(gSongLast).FirstNote Then
            gSongKey = 1
        End If
        gNote(Note).SongKey = gSongKey
    Else
        gSongKey = gNote(Note).SongKey
    End If
    Return
    
'**************************************
    
' E.g. if BeatUnits=8, at note positions 0, 8, 16
' the Beat will be set to 1, 2, 3.
SetTheBeat:
    N = 0
    gNote(Note).Beat = 0
    With gNote(Note).MeasTime
        For I = 0 To .Pos Step BeatUnits
            N = N + 1
            If I = .Pos Then
                gNote(Note).Beat = N
            End If
        Next I
    End With
    Return
    
End Sub


Private Sub FlagNHTHang()
    ' called by sub InputLines
    ' called by sub StoreSong
    ' if global gbNHTInputOption is true, from
    '   OP: MISC    NHTINPUT    ON

    ' calls function fcnNoteIndex
    ' Globals used:
    ' Globals set:

    ' From sub StoreSong:
    ' Do this at the end of a song but not before the
    ' first song.

    ' From sub InputLines:
    ' Do this at the end of all input cards.
    
    ' If a note that is input in either
    ' ABCPlus or 4Part format as a "partial chord",
    ' which contains less than four tones (A-G),
    ' then flag the note as a "hanging chord" with
    '   gNote(Note).xNHT "OR" NHTHANG
    ' Later the tones in the hanging chord will be
    ' checked to see if each is a harmonic or a
    ' non-harmonic tone (NHT).
    '
    ' Find the next "full chord", containing all four
    ' tones (A-G), before it and flag it with
    '   gNote(Note).xNHT "OR" NHTFIRST
    
    ' Sub Harmonize wiLL process these flags.

    Dim I As Integer
    Dim Note As Integer
    Dim NTones As Integer
    Dim bPossibleNHT As Boolean
    Dim Voice As Integer
    Dim Missing1 As Integer
    Dim FirstIx As Integer
    Dim FirstName As String
    Dim HangIx As Integer
    Dim HangName As String
    Dim bErr As Boolean
    Dim bHangNameFound As Boolean
    Dim Voice2 As Integer
    
    ' Sub StoreTM has already set
    '   gNote(Note).xNHT = NHTFIRST
    '   gNote(Note).xNHT = NHTHANG
    ' for all notes, so this Sub is probably
    ' not needed.
    
    Exit Sub ' 27sep08
    
    bPossibleNHT = False
    With gSong(gSongLast)
        For Note = .LastNote To .FirstNote Step -1
            'GoSub Test1
            If Left$(gNote(Note).TMName, 1) = "|" Then
                bPossibleNHT = False
            Else
                GoSub CountTones
                If NTones >= 4 Then
                    If bPossibleNHT Then
                        gNote(Note).xNHT = NHTFIRST 'set
                        bPossibleNHT = False
                    Else
                        gNote(Note).xNHT = NHTNONE 'set
                    End If
                ElseIf NTones = 3 Then
                    GoSub MissingVoice
                    If bHangNameFound Then
                        gNote(Note).xNHT = NHTFIRST 'set
                        bPossibleNHT = False
                    End If
                ElseIf NTones > 0 Then
                    gNote(Note).xNHT = NHTHANG 'set
                    bPossibleNHT = True
                Else ' nTones = 0 at bar or 4 rests
                    'gNote(Note).xNHT = NHTNONE 'set
                    bPossibleNHT = False
                End If
            End If
        Next Note
    End With
    Exit Sub
    
'**************************************

Test1:
    If Note = 57 Then
        I = I ' testing
    End If
    Return
    
'**************************************

' (1) Count the number of tones input for this note.

' Notice that sub StoreTM will propagate the
' time values ( Q E H etc) to the T1:T4 lines
' at labels CopyDefaults and
' CopyDefaultsToTheRest.
' This may make the second check (2) moot.

CountTones:
    NTones = 0
    For Voice = SOPRANO To BASS
        If gNote(Note).Voice(Voice).Index2Name = 0 Then
            Missing1 = Voice
        Else
            NTones = NTones + 1
        End If
    Next Voice
    Return

' NOT USED 27sep08
' (2) If less than four voice tones, then
' count the number of Tn: card names for this note.
            If gNote(Note).Voice(Voice).Time.Name = "" Then
                Missing1 = Voice
            Else
                NTones = NTones + 1
                I = I
            End If

'**************************************

' Count the number of tones input for this note.
MissingVoice:
    Voice = Missing1
    FirstIx = fcnNoteIndex(Note, Voice, bErr) ' call
    FirstIx = Abs(FirstIx) ' minus means user override
    If FirstIx > 0 Then
        FirstName = gNoteName(FirstIx)
    End If
    If FirstIx = 0 Then
        Debug.Print "FlagNHTHang: FirstIx=0"
        Stop
        I = I ' testing; should not happen
    Else ' If FirstIx <> 0 Then
        FirstName = gNoteName(FirstIx)
        ' Drop the trailing digit, e.g. C#4 -> C#
        FirstName = Left$(FirstName, Len(FirstName) - 1)
        GoSub FindNHTHangName
    End If
    Return

'*************

' For each Voice (or tone) with a non-zero index
' in thIS partial chord NHTHANG,
' look for a matching letter name (plus accidental)
' in the prior full chord NHTFIRST.  If found, this
' tone is harmonic. Ignore a difference in pitch.
' -1 means not a note: | or z
FindNHTHangName:
    bHangNameFound = False
    For Voice2 = SOPRANO To BASS
        HangIx = gNote(Note).Voice(Voice2).Index2Name
        If HangIx <= 0 Then ' 1sep08
            HangIx = fcnNoteIndex(Note, Voice2, bErr) ' call
            HangIx = Abs(HangIx) ' minus means user override
        End If
        If HangIx = 0 Then
            Debug.Print "FindNHTHang: HangIx=0"
            Stop: I = I ' testing
        Else
            HangName = gNoteName(HangIx)
            ' Drop the trailing digit, e.g. C#4 -> C#
            HangName = Left$(HangName, Len(HangName) - 1)
            If FirstName = HangName Then ' ignore pitch difference
                bHangNameFound = True
                Exit For
            End If
        End If
    Next Voice2
    Return

End Sub



Private Sub InitVoiceRangeDefaults()
    ' called by sub InputLines

    ' Globals used: gSongLast
    ' Globals set: gSong(Index).Range().

    ' At this point,
    '   gSongLast = gSongLast + 1
    ' has been updated to the new song
    ' at input card X:n

    Dim I As Integer
    
    If gSongLast = 1 Then
        ' set default values of Range.
        With gSong(gSongLast)
            For I = SOPRANO To BASS
                .RANGE(I).Top = gRangeDefault(I).Top
                .RANGE(I).Bot = gRangeDefault(I).Bot
                .RANGE(I).BestTop = gRangeDefault(I).BestTop
                .RANGE(I).BestBot = gRangeDefault(I).BestBot
            Next I
        End With
    End If

End Sub


Private Sub PropagateOptions()
    ' called by sub InputLines
    ' calls

    ' Globals used: gSongLast, gSongCurr
    ' Globals set: gSong(Index).bTriadAllowed,
    '   gSong(Index).RangeType

    ' Carry forward to the next song the
    ' options set in any previous song
    ' by line OP: ...
    ' in sub StoreNotes.

    ' At this point,
    '   gSongLast = gSongLast + 1
    ' has been updated to the new song
    ' at input card X:n

    Dim I As Integer
    Dim Index As Integer

    'gSong(gSongLast).BassGap = gBassGap
    gSong(gSongLast).BassGap = _
        gSong(gSongLast - 1).BassGap
    
    ' gxErrorAllowed=0 once in Init4Part.
    ' Propagate the current value to next song.
    'gSong(gSongLast).xErrorAllowed = gxErrorAllowed
    gSong(gSongLast).xErrorAllowed = _
        gSong(gSongLast - 1).xErrorAllowed
    gSong(gSongLast).xErrorAllowedPass1 = _
        gSong(gSongLast - 1).xErrorAllowedPass1
    
    ' Propagate the current value to next song.
    gSong(gSongLast).M = gSong(gSongLast - 1).M
    gSong(gSongLast).L = gSong(gSongLast - 1).L
    gSong(gSongLast).QText = gSong(gSongLast - 1).QText
        
    ' Propagate the current value to next song.
    For I = SOPRANO To BASS
        gSong(gSongLast).RANGE(I) = _
            gSong(gSongLast - 1).RANGE(I)
    Next I
    
    ' Propagate the current value to next song.
    For I = 0 To FIGBASSALLOWEDMAX
        gSong(gSongLast).bFigBassAllowed(I) = _
            gbFigBassAllowed(I) ' 11may08
        'gSong(gSongLast).bFigBassAllowed(I) = _
        '    gSong(gSongLast - 1).bFigBassAllowed(I)
    Next I
    
    Exit Sub
    
'****************************************

' TODO usable but not used.  Used on a note level.
    ' Propagate the current value to next song.
    For I = 1 To TRIADALLOWEDMAX
        'gSong(gSongLast).bTriadAllowed(I) = _
        '    gTriadAllowed(I)
        gSong(gSongLast).bTriadAllowed(I) = _
            gSong(gSongLast - 1).bTriadAllowed(I)
    Next I
    
'****************************************

' TODO usable but not used
        'For I = SOPRANO To BASS
        '    gSong(gSongLast).RANGE(I).Top = _
        '        gSong(gSongLast - 1).RANGE(I).Top
        '    gSong(gSongLast).RANGE(I).Bot = _
        '        gSong(gSongLast - 1).RANGE(I).Bot
        '    gSong(gSongLast).RANGE(I).BestTop = _
        '        gSong(gSongLast - 1).RANGE(I).BestTop
        '    gSong(gSongLast).RANGE(I).BestBot = _
        '        gSong(gSongLast - 1).RANGE(I).BestBot
        'Next I
End Sub




Private Sub ParseTabs(gTextLine As String)
    ' called by sub InputLines
    ' calls sub Parse

    ' Parse a whole input line, and store the
    ' values in gField().
    ' The fields are separated by a tab
    ' character, chr(9). First tab is in col 4.
    ' gField(0) is the identifier in col 1, e.g. V1:
    Const TABVAL As Integer = 9
    Const BLANKVAL As Integer = 32

    Static FieldLastSave ' keeps value between calls
    Dim I As Integer
    Dim A$
    Dim Col1 As Integer ' start of substring=tab+1
    Dim Col2 As Integer ' end of substring=tab
    Dim bErr As Boolean

'****************************************
'****************************************
Main:
    GoSub Init
    If Not bErr Then
        GoSub MainLine
    End If
    Exit Sub

'****************************************
'****************************************

    ' Extract the fields from the line.
    ' Strip off the continuation char "\"
    ' or chars " \"
    ' which is only used as a visual reminder.
MainLine:
    If Right$(gTextLine, 1) = "\" Then
        A$ = Trim$(Left$(gTextLine, _
            Len(gTextLine) - 1))
    Else
        A$ = Trim$(gTextLine)
    End If
    Call Parse(A$, gField(), FIELDMAX, _
        gFieldLast, TABVAL)
    If gFieldLast = 0 Then
        Call Parse(A$, gField(), FIELDMAX, _
            gFieldLast, BLANKVAL)
        If gFieldLast = 0 Then
            I = I
            Stop ' should not happen
        End If
    End If
    FieldLastSave = gFieldLast
    Return

'****************************************

Init:
    If Left$(gTextLine, 3) = "W1:" _
    Or Left$(gTextLine, 3) = "11:" Then
        I = I
    End If
    If FieldLastSave = 0 Then
        FieldLastSave = FIELDMAX
    End If
    'For I = 0 To FIELDMAX ' Clear the array
    For I = 0 To FieldLastSave ' Clear the array
        gField(I) = ""
    Next I

    ' First field is like "V1:[tab]".
    gFieldLast = 0
    If Len(gTextLine) < 5 Then
        bErr = True
        GoSub Err1
    Else
        gFieldLast = -1
    End If
    Return

'****************************************

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "ParseTabs: LineNo="; _
        gInputLineNo; "invalid length in "; _
        gTextLine
    Return
    Exit Sub

End Sub

Private Sub Parse(pText, pField() As String, _
        ByVal pMAX As Integer, _
        pFieldLast As Integer, _
        pSeparator As Integer)
    ' called by sub ParseTabs
    ' called by sub StoreFB
    ' calls
    ' No Globals

    Dim Col1 As Integer ' start of substring=tab+1
    Dim Col2 As Integer ' end of substring=tab
    Dim pSep As String * 1
    Dim Last As Integer
    Dim I As Integer

'***************************************
'***************************************

Main:
    If InStr(pText, "INCLUDE") > 0 _
    Or InStr(pText, "EXCLUDE") > 0 Then
        'Debug.Print pText
        Col1 = Col1
    End If
    GoSub Init
    GoSub MainLoop
    Exit Sub

'***************************************
'***************************************

MainLoop:
    Do While Col2 > 0
        If pFieldLast >= pMAX Then
            GoSub Err1
        Else
            pFieldLast = pFieldLast + 1
            pField(pFieldLast) = _
                Trim(Mid$(pText, Col1, Col2 - Col1))
            Col1 = Col2 + 1
            Col2 = InStr(Col1, pText, pSep)
        End If
    Loop
    
    ' Drop the last field if len=0, that is,
    ' it was one or more blanks followed
    ' by the pSeparator (usually a TAB).
    ' Keep dropping fields until the first
    ' non-empty one is found.
    ' This handles cases where the user
    ' has mistakenly input blanks or tabs
    ' at the end of a line. 14aug08
    Last = pFieldLast
    For I = Last To 1 Step -1
        If Len(pField(I)) = 0 Then
            pFieldLast = pFieldLast - 1
        Else
            Exit For
        End If
    Next I
    
    Return

'***************************************

Init:
    ' The fields are separated by a character,
    ' usually a tab chr(9) or a blank char(32).
    ' Add a separator at the end for parsing
    ' the last field.
    pSep = Chr$(pSeparator)
    pText = pText & pSep
    Col1 = 1 ' pos of prev tab+1
    Col2 = InStr(Col1, pText, pSep) ' pos of tab char
    Return

'***************************************

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "Parse: LineNo="; _
        gInputLineNo; _
        "reached field MAX="; pMAX; _
        "in "; pText
    Return
End Sub

Private Sub StoreNotes(gTextLine As String)
    ' called by sub InputLines

    ' calls function fcnGreater
    ' calls function fcnbKeyGood
    ' calls sub InitChordsForKey
    ' (calls sub InitKey)
    ' calls sub StoreOption
    ' calls sub StoreTM
    ' calls sub StoreVoiceVn
    ' calls sub StoreRN
    ' calls sub StoreFB
    ' calls sub StoreLyricWn ' for Wn:
    ' calls sub StoreLyricVoiceWn ' for 11: 12: ...
    ' calls sub StoreRaiseNoteHS

    ' Globals used: gSongLast,
    '   gSong(gSongLast), gField(I),
    '   gFieldLast,
    ' Globals set: gKeyName, gKeyMode, gSongKey,
    '   gNoteLast

    ' The sub ParseTabs has parsed the whole
    ' input line and stored the values in gField()
    ' trimmed of blanks and tabs.
    ' The fields were separated by a tab
    ' character, chr(9).
    ' The fields are not uppercased by sub Parse.

    ' A song may contain a "note" that is a
    ' single or double measure bar, "|" or "||".
    ' In this case sub StoreVoiceVn will set its
    ' index=0.

    ' All the non-OPTION cards must represent
    ' the same notes.  Using TAB's makes it easy
    ' to see that all the notes are lined up
    ' vertically.
    ' The TM cards are mandatory and cannot have
    ' any missing notes, and the song must end
    ' with a TM note = "||".

    ' The other cards must come after the TM card,
    ' and then there is no need to enter empty cards.
    ' Because:
    ' When a TM card is read, the .LastUsed(XX) values
    ' for all the the cards are reset to the
    '   fcnGreater(.LastTM and .LastUsed(XX).
    ' In this way, empty Vn, RN and FB cards
    ' do not need to be entered just to keep the
    ' right place for such cards later.

    ' A song may have up to four keys on four
    ' cards, KY: or K1:, K2:, K3: or K4:.
    ' These cards must occur before the notes.
    ' If a Kn: occurs on a RN: card for a note,
    ' then the chords are switched to that key
    ' from that note on, until the next modulation
    ' key, e.g. K1:, is reached.
    ' The song always begins with gSongKey=1.
    
    ' If K2: is entered at chord n, then any accented
    ' note for that chord is entered as, e.g. G#3.
    ' Even if G# is in the key signature, entering
    ' G3 will not give you G#3 while K2: is in effect.
    ' This is so that the display will agree with the
    ' printed score.

    Dim RecType As String
    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Const KEYMAX = 3
    Dim Key(KEYMAX) As String
    Dim KeyMode As Integer
    Dim Type1 As Integer
    Dim Letter1$
    Dim KeyNo As Integer
    Dim bErr As Boolean
    Dim VoiceLyric As Integer
    Dim LyricLine As Integer

'*****************************************
'*****************************************

Main:
    GoSub Init
    GoSub GetCardType1 ' col 1-2
    GoSub MainSelect
        'Case "KY", "K1" To "K4"
            'GoSub UpperCase
            'fcnbKeyGood
            'GoSub StoreKeyNo
                'GoSub Err2
                'GoSub Err3
                'GoSub PropagateParms
            'call InitChordsForKey
        'Case "OP"
            'call StoreOption(gTextLine)
        'Case "TM"
            'GoSub BumpAll ' reset the others
            'call StoreTM(.LastUsed(TM), gTextLine)
            'GoSub BumpLast
        'Case "T1", "T2", "T3", "T4"
            'call StoreTM(Type1 - 5, .LastUsed(Type1), _
            'GoSub BumpLast
        'Case "V1", "V2", "V3", "V4"
            'call StoreVoiceVn(Type1 - 5, .LastUsed(Type1), _
            'GoSub BumpLast
        'Case "RN"
            'call StoreRN(.LastUsed(RN), gTextLine)
            'GoSub BumpLast
        'Case "FB"
            'call StoreFB(.LastUsed(FB), gTextLine)
            'GoSub BumpLast
        'Case "W1", "W2", "W3", "W4"
            'call StoreLyricWn(Type1 - 11, .LastUsed(Type1), _
            'GoSub BumpLast
        'Case "11", "12", "13", "14"
        'Case "ER"
        'Case Else
            'GoSub Err1
    Exit Sub

'*****************************************
'*****************************************

Init:
    ' For insurance, truncate the input user line
    ' after any "|| \" or || \".
    I = InStr(gTextLine, "| \")
    If I > 0 Then
        gTextLine = Left$(gTextLine, I + 2)
    End If
    Return

'*****************************************

GetCardType1:
    RecType = Left$(gTextLine, 2) ' e.g. KY: OP: V1:
    RecType = UCase$(RecType)
    If RecType = "11" Then
        I = I
    End If
    ' Get the type of card into Type1.  E.g.
    ' in the LASTNAMES array,
    ' the two digits before the name "V4" are "04".
    ' e.g. if type is "V4", then type1=const V4 = 04
    I = InStr(LASTNAMES, RecType & " ")
    If I > 0 Then
        Type1 = Val(Mid$(LASTNAMES, I - 2, 2))
    Else
        Type1 = 99 ' err
    End If
    Return

'*****************************************

MainSelect:
    With gSong(gSongLast)
        Select Case RecType
        ' K2 to K4 are optional modulation keys, e.g.
        ' K2: C MAJOR
        ' K3: C# MINOR NATURAL
        Case "KY", "K1" To "K5"
            grdKey.Col = 0 ' TODO not needed?
            .LastNoteTM = MAXINTEGER ' 32K
            GoSub UpperCase ' get Key(1) and uppercase.
            ' Get KeyMode:
            If fcnbKeyGood(Key(), KeyMode) Then ' call
                GoSub StoreKeyNo
                If Not bErr Then
                    If RecType = "KY" Then ' 17jul08
                        Call InitChordsForKey
                    End If
                End If
            End If
        Case "OP"
            Call StoreOption(gTextLine)
        Case "TM"
            GoSub BumpAll ' reset the other Types
            Call StoreTM(Type1, .LastUsed(Type1), _
                            gTextLine)
            J = .LastUsed(Type1)
            If InStr(gTextLine, "||") > 0 Then
                .LastNoteTM = J
            End If
            GoSub BumpLast
        Case "T1", "T2", "T3", "T4"
            GoSub CheckCommentTimes
            If Not bErr Then
            ' Type1 = const T1 to T4 = 2 to 5
            ' Subtract 01 from the type nos 02-05 to
            ' get Tn: numbers 01-04.
            'call StoreTM(Type1 - 1, .LastUsed(Type1), _
            '                gTextLine)
                Call StoreTM(Type1, .LastUsed(Type1), _
                                gTextLine)
                J = .LastUsed(Type1)
                GoSub BumpLast
            End If
        Case "V1", "V2", "V3", "V4"
            GoSub CheckCommentVoices
            If Not bErr Then
                ' Type1 = const V1 to V4 = 1 to 4
                ' Subtract 05 from the type nos 06-09 to
                ' pass 01-04.  See LASTNAMES array.
                Call StoreVoiceVn(Type1 - 5, _
                        .LastUsed(Type1), gTextLine)
                J = .LastUsed(Type1)
                GoSub BumpLast
            End If
        Case "RN"
            Call StoreRN(.LastUsed(RN), gTextLine)
            J = .LastUsed(RN)
            GoSub BumpLast
        Case "FB"
            Call StoreFB(.LastUsed(FB), gTextLine)
            J = .LastUsed(FB)
            GoSub BumpLast
        Case "W1", "W2", "W3", "W4"
            ' Type1 = const W1 to W4 = 1 to 4
            ' Subtract 11 from the type nos 12-15 to
            ' pass 01-04.
            Call StoreLyricWn(Type1 - 11, .LastUsed(Type1), _
                            gTextLine)
            J = .LastUsed(Type1)
            GoSub BumpLast
        Case "1I", "11", "12", "13", "14"
            If RecType = "1I" Then
                RecType = "11" ' distinguish 1I from I1
            End If
            GoSub CheckCommentLyrics
            If Not bErr Then
                gbOneLyricLinePerVoice = False ' see DisplayNotes
                LyricLine = Val(Left$(RecType, 1))
                VoiceLyric = Val(Right$(RecType, 1))
                Call StoreLyricVoiceWn(VoiceLyric, LyricLine, _
                                .LastUsed(Type1), gTextLine)
                J = .LastUsed(Type1)
                GoSub BumpLast
            End If
        Case "I1", "11", "21", "31", "41"
            If RecType = "I1" Then
                RecType = "11" ' distinguish 1I from I1
            End If
            GoSub CheckCommentLyrics
            If Not bErr Then
            ' Notice case "11" will use Type1=16, not 20.
                gbOneLyricLinePerVoice = True ' see DisplayNotes
                LyricLine = Val(Right$(RecType, 1))
                VoiceLyric = Val(Left$(RecType, 1))
                Call StoreLyricVoiceWn(VoiceLyric, LyricLine, _
                                .LastUsed(Type1), gTextLine)
                J = .LastUsed(Type1)
                GoSub BumpLast
            End If
        Case "ER"
            Call StoreErrorER(.LastUsed(ER), _
                  gTextLine)
            J = .LastUsed(ER)
            GoSub BumpLast
        Case "HS"
            Call StoreRaiseNoteHS(.LastUsed(HS), _
                  gTextLine)
            J = .LastUsed(HS)
            GoSub BumpLast
        Case "XX"
            Call StoreChangeXZRests(.LastUsed(XX), _
                  gTextLine)
            J = .LastUsed(XX)
            GoSub BumpLast
        Case "ZZ"
            Call StoreChangeXZRests(.LastUsed(ZZ), _
                  gTextLine)
            J = .LastUsed(ZZ)
            GoSub BumpLast
        Case Else
            GoSub Err1
        End Select
        Return
    End With
    
'*****************************************

' if V1 V2 V3 or V4 were specified on the
'   OP: MISC    COMMENT V1  V2
' card, then bypass it. 28sep08

CheckCommentVoices:
    If InStr(gCommentVoices, RecType) Then
        bErr = True
    Else
        bErr = False
    End If
    Return
    
'*****************************************

' if "T1", "T2", "T3", "T4"
' were specified on the
'   OP: MISC    COMMENT T1  T2
' card, then bypass it. 28sep08

CheckCommentTimes:
    If InStr(gCommentTimes, RecType) Then
        bErr = True
    Else
        bErr = False
    End If
    Return
    
'*****************************************

' if "11", "12", "13", "14"
' or "11", "21", "31", "41"
' were specified on the
'   OP: MISC    COMMENT 11  12      or
'   OP: MISC    COMMENT 21  31
' card, then bypass it. 28sep08

CheckCommentLyrics:
    If InStr(gCommentLyrics, RecType) Then
        bErr = True
    Else
        bErr = False
    End If
    Return
    
'*****************************************

UpperCase:
     ' Uppercase the Key letter, followed by
     ' any # or b not uppercased, eg. Cb.
     Letter1$ = UCase$(Left$(gField(1), 1))
     Key(1) = Letter1$ & Mid$(gField(1), 2)

     ' Uppercase the other fields e.g.
     ' K2: C# Minor Melodic
     For I = 2 To gFieldLast
         Key(I) = UCase$(gField(I))
     Next I
     ' TODO cleanup
    'For I = 1 To _
    'fcnGreater(gFieldLast, 3) ' call
    '    Letter$ = UCase$(Left$(gField(I), 1))
    '    Key(I) = Letter$ & Mid$(gField(I), 2)
    'Next I
     Return

'*****************************************
' Store the key info, which starts in col 1, e.g.
'   KY: C Major
'   K2: G Minor Natural
' Notice fcnbKeyGood converted strings Key(2)
' and Key(3) to integer Keymode.
StoreKeyNo:
    With gSong(gSongLast)
        KeyNo = Val(Right$(RecType, 1)) ' K1:-K4:
        If KeyNo = 0 Then
            KeyNo = 1 ' KY: = K1:
            .NumKey = 1
        End If
        If KeyNo > NUMKEYMAX Then
            bErr = True
            GoSub Err2
        Else
            If KeyNo > .NumKey Then
                .NumKey = KeyNo ' # keys in song
            End If
            .Key(KeyNo).Name = Key(1)
            .Key(KeyNo).Mode = KeyMode
            ' Do not confuse Key(1) with .Key(1) here.
            ' KeyNo = 1 should be input first:
            If .Key(1).Name = "" Then
                bErr = True
                GoSub Err3
            Else
                ' Get gKeyName and gKeyMode from
                ' the key signature, not the
                ' modulation keys K2: etc.
                ' Convert fixed string*2 to string
                ' by adding a trailing blank:
                gKeyName = Trim$ _
                    (.Key(1).Name & " ")
                gKeyMode = .Key(1).Mode
                gSongKey = 1
                GoSub PropagateParms
            End If
        End If
        If gbAbcInputOption Then
            ' Save 4Part .Key data in loc=0 for
            ' use at ABC format K:c card in
            ' sub StoreSong
            If gSongLast > 0 Then
                For I = 1 To NUMKEYMAX
                    gSong(0).Key(I) = _
                        gSong(gSongLast).Key(I)
                    gSong(0).Key(I).Name = _
                        gSong(gSongLast).Key(I).Name
                    gSong(0).Key(I).Mode = _
                        gSong(gSongLast).Key(I).Mode
                Next I
            End If
        End If
    End With
    Return

'*****************************************

PropagateParms:
    ' See Sub FillWithKeys:
    ' propagate to all notes in the song
    ' until a new modulation key is entered.
    ' TODO may not be needed.

    gSong(gSongLast).BassGap = gBassGap
    Return

'*****************************************

' Bring all of the last used pointers of the other
' cards up to the level of the TM cards.  This
' means you do not have to enter empty RN or FB cards.
BumpAll:
    With gSong(gSongLast)
        For I = 1 To LASTUSEDMAX
            If .LastUsed(I) < .LastUsed(TM) Then
                .LastUsed(I) = .LastUsed(TM)
            End If
        Next I
    End With
    Return

'*****************************************

BumpLast:
    With gSong(gSongLast)
        If J > .LastNote Then
           'If J > .LastNoteTM Then
           '    J = J
           '    Stop
           'Else
                .LastNote = J
                gNoteLast = J
           'End If
        End If
        Return
    End With

'*****************************************

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreNotes:E1 LineNo="; _
        gInputLineNo; "col.1 invalid in "; _
        gTextLine
    Debug.Print "E1: len(gTextLine)="; Len(gTextLine)
    Return

'*****************************************

Err2:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreNotes:E2 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "RecType="; RecType; _
        ", keyname > K4 in "; gTextLine
    Return

'*****************************************

Err3:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreNotes:E3 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "Missing KY: or K1: should be first. "; _
        "Found RecType="; _
        RecType; " in "; gTextLine
    Return

'*****************************************
' NOT USED:

DebugLast:
    With gSong(gSongLast): For K = 1 To 11: Debug.Print "StoreNotes:E4 "; K; .LastUsed(K); " ";: Next K: End With
    Return

End Sub


Private Function fcnbKeyGood(pKey() As String, _
                    pKeyMode As Integer) _
                    As Boolean
    ' called by sub StoreNotes
    ' pKey() fields are in uppercase.
    Dim Key1 As String
    Dim Key2 As String
    Dim Valid As String
    Valid = "ABCDEFGA#B#C#D#E#F#G#AbBbCbDbEbFbGb"

    fcnbKeyGood = False
    With gSong(gSongLast)
        Key1 = Trim$(pKey(1))
        If InStr(Valid, Key1) = 0 Then
            GoTo Err1
        End If

        Key2 = Trim$(pKey(2))
        If Key2 = "MAJOR" Then
            pKeyMode = MAJOR
            fcnbKeyGood = True
            Exit Function
        ElseIf Key2 = "MINOR" Then
            Select Case Trim$(pKey(3))
                Case "NATURAL"
                    pKeyMode = MINORNAT
                    fcnbKeyGood = True
                    Exit Function
                Case "HARMONIC"
                    pKeyMode = MINORHARM
                    fcnbKeyGood = True
                    Exit Function
                Case "MELODIC"
                    pKeyMode = MINORMELUP
                    fcnbKeyGood = True
                    Exit Function
                Case Else
                    GoTo Err3
            End Select
        ElseIf Key2 = "DORIAN" Then
            pKeyMode = DORIAN
            fcnbKeyGood = True
            Exit Function
        ElseIf Key2 = "PHRYGIAN" Then
            pKeyMode = PHRYGIAN
            fcnbKeyGood = True
            Exit Function
        ElseIf Key2 = "LYDIAN" Then
            pKeyMode = LYDIAN
            fcnbKeyGood = True
            Exit Function
        ElseIf Key2 = "MIXOLYDIAN" Then
            pKeyMode = MIXOLYDIAN
            fcnbKeyGood = True
            Exit Function
        Else
            GoTo Err2
        End If
    End With
    Exit Function

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "fcnbKeyGood: invalid keyname="; _
        pKey(1); " in song "; gSongLast
    Exit Function
Err2:
    gInputErrs = gInputErrs + 1
    Debug.Print "fcnbKeyGood: missing MAJOR, "; _
        "MINOR, DORIAN, PHRYGIAN, LYDIAN, or "; _
        "MIXOLYDIAN in Kn: line for song"; gSongLast
    Exit Function
Err3:
    gInputErrs = gInputErrs + 1
    Debug.Print "fcnbKeyGood: missing NATURAL, "; _
        "HARMONIC, or MELODIC"; _
        " in Kn: line for MINOR song "; gSongLast
    Exit Function
End Function

Private Sub InitHalfStepNames()
    ' called by sub InitChordsForKey
    ' called by sub InitNoteNamesCMajor
    
    ' Globals used: gKeyMode, gKeyName
    ' Globals set: gHalfStepName0(I),
    '   gbSharpKey, gHalfStepName(J)

    ' Initialize the Variant gHalfStepName array with
    ' the note string names.
    ' Use this technique instead of READ DATA
    ' which QBASIC has but not Visual Basic.

    ' TODO only execute the ARRAY functions
    ' that are needed.

    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim Sharps ' variant initialized by an array.
    Dim Sharps6 ' "
    Dim Sharps7 ' "
    Dim Flats  ' "
    Dim Flats6  ' "
    Dim Flats7  ' "
    Dim MajorKey As Variant
    Dim MinorKey As Variant
    Dim DorianKey As Variant
    Dim PhrygianKey As Variant
    Dim LydianKey As Variant
    Dim MixolydianKey As Variant
    Dim NumFlatSharp As Variant
    Dim NumFlats As Integer
    Dim NumSharps As Integer
    Dim Prev1 As Integer
    Dim Next1 As Integer
    Dim A$
    Dim B$
    Dim E$
    Dim KeySigSharps As Variant
    Dim KeySigFlats As Variant
    Dim bFoundKey As Boolean
    Dim Song As Integer
    Dim HalfStepName(0 To 12) As String ' temp

    
    ' TODO
    'If gSongLast = 4 Then
    '    I = I
    'End If
    MajorKey = Array _
        ("Cb", "Gb", "Db", "Ab", "Eb", "Bb", "F", _
        "C", "G", "D", "A", "E", "B", "F#", "C#")
    '     7b    6b    5b    4b    3b    2b    1b
    '    0#   1#   2#   3#   4#   5#   6#    7#
    
    NumFlatSharp = Array _
        (-7, -6, -5, -4, -3, -2, -1, _
        0, 1, 2, 3, 4, 5, 6, 7)
    
    MinorKey = Array _
        ("Ab", "Eb", "Bb", "F", "C", "G", "D", _
        "A", "E", "B", "F#", "C#", "G#", "D#", "A#")
    '     7b    6b    5b    4b   3b   2b   1b
    '    0#   1#   2#   3#   4#     5#    6#    7#

    '0#  1#    2#    3#    4#    5#    6#    7#
    KeySigSharps = Array( _
    "", "F#", "C#", "G#", "D#", "A#", "E#", "B#")
    
    '0b  1b    2b    3b    4b    5b    6b    7b
    KeySigFlats = Array( _
    "", "Bb", "Eb", "Ab", "Db", "Gb", "Cb", "Fb")

    ' When Dorian starts on D, it has the same
    ' key signature as C Major, etc.
    DorianKey = Array _
        ("Db", "Ab", "Eb", "Bb", "F", "C", "G", _
        "D", "A", "E", "B", "F#", "C#", "G#", "D#")
    '     7b    6b    5b    4b    3b   2b   1b
    '    0#   1#   2#   3#   4#    5#    6#    7#
    ' When Phrygian starts on E, it has the same
    ' key signature as C Major, etc.
    PhrygianKey = Array _
        ("Eb", "Bb", "F", "C", "G", "D", "A", _
        "E", "B", "F#", "C#", "G#", "D#", "A#", "E#")
    '     7b    6b    5b   4b   3b   2b   1b
    '    0#   1#   2#    3#    4#    5#    6#    7#
    ' When Lydian starts on F, it has the same
    ' key signature as C Major, etc.
    LydianKey = Array _
        ("Fb", "Cb", "Gb", "Db", "Ab", "Eb", "Bb", _
        "F", "C", "G", "D", "A", "E", "B", "F#")
    '     7b    6b    5b    4b    3b    2b    1b
    '    0#   1#   2#   3#   4#   5#   6#   7#
    ' When Mixolydian starts on G, it has the same
    ' key signature as C Major, etc.
    MixolydianKey = Array _
        ("Gb", "Db", "Ab", "Eb", "Bb", "F", "C", _
        "G", "D", "A", "E", "B", "F#", "C#", "G#")
    '     7b    6b    5b    4b     3b   2b   1b
    '    0#   1#   2#   3#   4#   5#    6#    7#

    Sharps = Array("C", "C#", "D", "D#", "E", _
        "F", "F#", "G", "G#", "A", "A#", "B", "C")
    ' Change F to enharmonic E#:
    Sharps6 = Array("C", "C#", "D", "D#", "E", _
        "E#", "F#", "G", "G#", "A", "A#", "B", "C")
    ' Change F to enharmonic E#, and C to B#:
    Sharps7 = Array("B#", "C#", "D", "D#", "E", _
        "E#", "F#", "G", "G#", "A", "A#", "B", "B#")

    Flats = Array("C", "Db", "D", "Eb", "E", _
        "F", "Gb", "G", "Ab", "A", "Bb", "B", "C")
    ' Change B to enharmonic Cb:
    Flats6 = Array("C", "Db", "D", "Eb", "E", _
        "F", "Gb", "G", "Ab", "A", "Bb", "Cb", "C")
    ' Change B to enharmonic Cb, and E to Fb:
    Flats7 = Array("C", "Db", "D", "Eb", "Fb", _
        "F", "Gb", "G", "Ab", "A", "Bb", "Cb", "C")

'**********************************************
'**********************************************

Main:
    GoSub Init
    GoSub MainLine
    Exit Sub

'**********************************************
'**********************************************
    
Init:
    ' "normal" chromatic scale for use in fcnToIx
    ' I think this is for the SATB voice ranges which
    ' are given in sharps, before any songs are
    ' input.
    ' TODO 12/10/6.  Probably no longer needed.
    ' See sub InitNoteNamesCMajor.
    For I = 0 To 11
        gHalfStepName0(I) = Sharps(I)
    Next I
    Return
    
'**********************************************
    
MainLine:
    ' Select the chromatic scale for use in fcnToIx
    ' and InitNoteNames.
    bFoundKey = False
    Select Case gKeyMode
        Case MAJOR
            For I = 0 To 14
                If gKeyName = MajorKey(I) Then
                    bFoundKey = True
                    GoSub Copy
                    Exit For
                End If
            Next I
        Case MINORNAT, MINORHARM, MINORMELUP
            For I = 0 To 14
                If gKeyName = MinorKey(I) Then
                    bFoundKey = True
                    GoSub Copy
                    Exit For
                End If
            Next I
        Case DORIAN
            For I = 0 To 14
                If gKeyName = DorianKey(I) Then
                    bFoundKey = True
                    GoSub Copy
                    Exit For
                End If
            Next I
        Case PHRYGIAN
            For I = 0 To 14
                If gKeyName = PhrygianKey(I) Then
                    bFoundKey = True
                    GoSub Copy
                    Exit For
                End If
            Next I
        Case LYDIAN
            For I = 0 To 14
                If gKeyName = LydianKey(I) Then
                    bFoundKey = True
                    GoSub Copy
                    Exit For
                End If
            Next I
        Case MIXOLYDIAN
            For I = 0 To 14
                If gKeyName = MixolydianKey(I) Then
                    bFoundKey = True
                    GoSub Copy
                    Exit For
                End If
            Next I
        Case Else
            GoTo Err2
    End Select
    If Not bFoundKey Then
        GoTo Err1
    End If
    Return
    'Exit Sub

'**********************************************
' The range 0-12 instead of 0-11 is needed for
' AdjustNaturals:
Copy:
    Select Case NumFlatSharp(I)
    Case -7
        gbSharpKey = False
        For J = 0 To 12
            HalfStepName(J) = Flats7(J)
        Next J
    Case -6
        gbSharpKey = False
        For J = 0 To 12
            HalfStepName(J) = Flats6(J)
        Next J
    'Case Is > -6 And (NumFlatSharp(I) < 0)
    Case -5 To -1
        gbSharpKey = False
        For J = 0 To 12
            HalfStepName(J) = Flats(J)
        Next J
    Case 6
        gbSharpKey = True
        For J = 0 To 12
            HalfStepName(J) = Sharps6(J)
        Next J
    Case 7
        gbSharpKey = True
        For J = 0 To 12
            HalfStepName(J) = Sharps7(J)
        Next J
    Case Else ' 0 to 5
        gbSharpKey = True
        For J = 0 To 12
            HalfStepName(J) = Sharps(J)
        Next J
    End Select
    For J = 0 To 11
        gHalfStepName(J) = HalfStepName(J)
    Next J
    GoSub AdjustPicardy  ' 17jul08
    GoSub AdjustNaturals  ' 17jul08
    'GoSub Debug1
    'GoSub Debug2
    Return

'**********************************************
' In minor keys, need to allow input of a raised
' third in the last chord, a Picardy chord.
' By inspection, this seems to affect only two
' sharp minor keys: C# (4 sharps) and G# (5 sharps).
AdjustPicardy:
    If gKeyMode = MINORNAT Then
        If gKeyName = "C#" Then
            If gHalfStepName(5) = "F" Then
                gHalfStepName(5) = "E#"
            Else
                Stop
            End If
            If gHalfStepName(0) = "C" Then
                gHalfStepName(0) = "B#"
            Else
                Stop
            End If
        ElseIf gKeyName = "G#" Then
            If gHalfStepName(0) = "C" Then
                gHalfStepName(0) = "B#"
            Else
                Stop
            End If
            'If gHalfStepName(12) = "C" Then
            '    gHalfStepName(12) = "B#"
            If gHalfStepName(5) = "F" Then ' 14aug08
                gHalfStepName(5) = "E#"
            Else
                Stop
            End If
        End If
    End If
    Return
    
'**********************************************
' I decided to copy the adjusted natural
' sign notes to gHalfStepNameNatural.
' I probably won't use them since there is
' another way to do it. 2feb08
AdjustNaturals:
    If NumFlatSharp(I) < 0 Then
        NumFlats = Abs(NumFlatSharp(I))
        GoSub NaturalizeFlats
    ElseIf NumFlatSharp(I) > 0 Then
        NumSharps = NumFlatSharp(I)
        GoSub NaturalizeSharps
    End If
    'GoSub Debug1
    For J = 1 To 11
        'gHalfStepNameNatural(J) = HalfStepName(J)
        gHalfStepName(J) = HalfStepName(J) ' 17jul08
    Next J
    'gHalfStepNameNatural(0) = HalfStepName(12)
    gHalfStepName(0) = HalfStepName(12) ' 17jul08
    Return
    
'**********************************************
' See note at NaturalizeSharps:
NaturalizeFlats:
    For K = 1 To NumFlats
        A$ = KeySigFlats(K)
        For J = 0 To 11
            If A$ = HalfStepName(J) Then
                B$ = HalfStepName(J + 1)
                If B$ = Left$(A$, 1) Then
                    HalfStepName(J + 1) = "=" & B$
                End If
            End If
        Next J
    Next K
    Return
    
'**********************************************
' Each sharp scale contains the 7 diatonic tones
' as well as the 5 sharps. Need to allow the user to
' input a natural, e.g. =F in the key of G major.
' If an input note, e.g. F in G major, has a sharp
' in the key signature, then it will have a sharp
' in HalfStepName, that is, F#.
' Now if the note just below it in HalfStepName
' is just the single letter, F, add an "=" in
' front of it in HalfStepName.
'  G G# A A# B C C# D D# E F F# G
'  G    A    B C    D    E   F# G
'                         =F
' Note that this doesn't work for keys with 6
' or 7 sharps, where we have B# C# or E# F#
' Here, the user would naturally input B#
' instead of =C, and E# instead of =F.
NaturalizeSharps:
    For K = 1 To NumSharps
        A$ = KeySigSharps(K)
        For J = 1 To 12
            If A$ = HalfStepName(J) Then
                B$ = HalfStepName(J - 1)
                If B$ = Left$(A$, 1) Then
                    HalfStepName(J - 1) = "=" & B$
                End If
            End If
        Next J
    Next K
    Return
    
'**********************************************
    
Err1:
    Debug.Print "InitHalfStepNames: invalid key="; _
        gKeyName; " for mode="; gKeyMode; _
        " in song=";
    If gSongCurr > 0 Then
        Debug.Print gSongCurr
    Else
        Debug.Print gSongLast
    End If
    Stop

'**********************************************
    
Err2:
    Debug.Print "InitHalfStepNames: invalid keymode="; _
        gKeyMode; " in song=";
    If gSongCurr > 0 Then
        Debug.Print gSongCurr
    Else
        Debug.Print gSongLast
    End If
    Stop
    I = I ' testing
    
'**********************************************
' not needed
Err3:
    If gSongCurr > 0 Then
        Song = gSongCurr
    Else
        Song = gSongLast
    End If
    E$ = "InitHalfStepNames: " _
        & "invalid natural for " & B$ & " for key=" _
        & gKeyName & " for mode=" & gKeyMode _
        & " in song=" & Song
    Debug.Print E$
    Call DisplayText(E$)
    Return

'**********************************************
    
Debug1:
    If gSongCurr > 0 Then
        Song = gSongCurr
    Else
        Song = gSongLast
    End If
    E$ = "InitHalfStepNames: " _
        & "naturals for key=" _
        & gKeyName & " for mode=" & gKeyMode _
        & " in song=" & Song
    Call DisplayText(E$)
    Debug.Print E$
    Debug.Print "gHalfStepName= ";
    For J = 0 To 11
        Debug.Print gHalfStepName(J);
    Next J
    Debug.Print
    Return
'**********************************************
    
Debug2:
    If gSongCurr > 0 Then
        Song = gSongCurr
    Else
        Song = gSongLast
    End If
    E$ = "InitHalfStepNames: " _
        & "naturals for key=" _
        & gKeyName & " for mode=" & gKeyMode _
        & " in song=" & Song
    Call DisplayText(E$)
    Debug.Print E$
    Debug.Print "gHalfStepNameNatural= ";
    For J = 0 To 11
        Debug.Print gHalfStepNameNatural(J);
    Next J
    Debug.Print
    Return

    End Sub

Private Sub InitVoiceRange()
    ' called by sub StoreNotes
    ' called by sub cmdChords_Click
    ' calls

    ' Use the fcnToIxCMajor function to get the
    ' starting and ending indexes of the
    ' ranges of the soprano, alto, tenor,
    ' bass voices.

    ' TODO - may only need to call this once,
    '       unless I add a range control input
    '       card for each song.

    ' TODO must initialize gS1 etc as defaults.

    ' Set by option line OP: RANGE TYPEn
    ' in sub StoreRange.
    ' NOTE - C3=Middle C

    ' NOTE - gSongLast is used in sub InputLines,
    ' where gSongCurr = 0. Afterwards, gSongCurr
    ' is set in sub Main4Part.
    Dim Index As Integer
    Dim A As String * 3
    Dim B$
    Dim C$
    Dim Z As Integer
    Dim I As Integer
    Dim J As Integer
    Dim Blank3 As String * 3

    If gSongCurr = 0 Then
        Index = gSongLast ' sub InputLines
    Else
        Index = gSongCurr ' sub Main4Part
    End If

    ' Can lower soprano to C4 and alto to G3
    '   to support "Joy to the World".
    ' Can lower bass to F2 to support
    '   Ottman 5.8(4) p 98.

    ' Normal ranges, Ottman p86
    ' Ottman's names:       This program's names:
    ' Soprano:  d1 to g2    D4 to G5 (C4=Middle C)
    ' Alto:     a  to c2    A3 to C5
    ' Tenor:    e  to g1    D3 to G4
    ' Bass:     G  to d1    G2 to D4

    ' If gRangeMax Then
    ' Extended ranges, Ottman p86
    ' Ottman's names:       This program's names:
    ' Soprano:  c1 to a2    C4 to A5 (C4=Middle C)
    ' Alto:     f  to e2    F3 to E5
    ' Tenor:    d  to a1    D3 to A4
    ' Bass:     D  to e1    D2 to E4

    With gSong(Index)
        For J = SOPRANO To BASS
            A = .RANGE(J).Bot ' string
            GoSub C
            .RangeBot(J) = Z ' integer
            gRange(J).Bot = Z

            A = .RANGE(J).Top
            GoSub C
            .RangeTop(J) = Z
            gRange(J).Top = Z

            A = .RANGE(J).BestBot
            GoSub C
            .RangeBestBot(J) = Z

            A = .RANGE(J).BestTop
            GoSub C
            .RangeBestTop(J) = Z
        Next J

        ' gS1 to gB2 are used in sub ScanTriads
        gS1 = .RangeBot(SOPRANO)
        gA1 = .RangeBot(ALTO)
        gT1 = .RangeBot(TENOR)
        gB1 = .RangeBot(BASS)

        gS2 = .RangeTop(SOPRANO)
        gA2 = .RangeTop(ALTO)
        gT2 = .RangeTop(TENOR)
        gB2 = .RangeTop(BASS)

        gBestS1 = .RangeBestBot(SOPRANO)
        gBestA1 = .RangeBestBot(ALTO)
        gBestT1 = .RangeBestBot(TENOR)
        gBestB1 = .RangeBestBot(BASS)

        gBestS2 = .RangeBestTop(SOPRANO)
        gBestA2 = .RangeBestTop(ALTO)
        gBestT2 = .RangeBestTop(TENOR)
        gBestB2 = .RangeBestTop(BASS)
    End With

    Exit Sub

' Note a fixed string will always stay a fixed
' string no matter what it is copied to, and TRIM
' not work on it.  You must convert a fixed string
' to an ordinary string variable (of varying
' length) by adding a byte to it in order to use
' TRIM on it.

C:
    I = I + 1 ' counter in case of failure
    B$ = A & " " ' convert to ordinary string
    C$ = Trim$(B$)
    Z = fcnToIxCMajor(C$)
    If Z = 99 Then
        GoTo Err1
    End If
    Return

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "InitVoiceRange: invalid note="; _
        C$; " for voice no.="; J; _
        " in song no="; gSongLast
    Stop

End Sub


Private Sub InitRangeNames()
    ' called by sub Main4Part
    
    ' Use the fcnToIxCMajor function to get the
    ' starting and ending indexes of the
    ' ranges of the soprano, alto, tenor,
    ' bass voices.

    ' NOTE - C4=Middle C

    Dim I As Integer
    Dim Blank3 As String * 3
    
    Dim DefaultTop As Variant
    Dim DefaultBot As Variant
    Dim DefaultBestTop As Variant
    Dim DefaultBestBot As Variant

    ' Can lower soprano to C4 and alto to G3
    '   to support "Joy to the World".
    ' Can lower bass to F2 to support
    '   Ottman 5.8(4) p 98.

    ' Normal ranges, Ottman p86
    ' Ottman's names:       This program's names:
    ' Soprano:  d1 to g2    D4 to G5 (C4=Middle C)
    ' Alto:     a  to c2    A3 to C5
    ' Tenor:    e  to g1    D3 to G4
    ' Bass:     G  to d1    G2 to D4

    ' If gRangeMax Then the (unused) InitVoiceEffort
    ' uses Extended ranges, Ottman p86
    ' Ottman's names:       This program's names:
    ' Soprano:  c1 to a2    C4 to A5 (C4=Middle C)
    ' Alto:     f  to e2    F3 to E5
    ' Tenor:    d  to a1    D3 to A4
    ' Bass:     D  to e1    D2 to E4

' Note a fixed string like
'     gRangeDefault(I).Top
' will always stay a fixed
' string no matter what it is copied to, and TRIM
' not work on it.  You must convert a fixed string
' to an ordinary string variable (of varying
' length) by adding a byte to it in order to use
' TRIM on it.
    
    DefaultTop = Array("", "G5", "C5", "G4", "D4")
    DefaultBot = Array("", "D4", "A3", "D3", "G2")
    DefaultBestTop = Array("", "G5", "C5", "G4", "D4")
    DefaultBestBot = Array("", "D4", "A3", "D3", "G2")
    
    For I = SOPRANO To BASS
        gRangeDefault(I).Top = DefaultTop(I)
        gRangeDefault(I).Bot = DefaultBot(I)
        gRangeDefault(I).BestTop = DefaultBestTop(I)
        gRangeDefault(I).BestBot = DefaultBestBot(I)
    Next I
    
End Sub



Private Sub InitVoiceEffort()

    ' NO LONGER USED !

    ' called by sub InitVoiceRange
    ' (Called by sub InitChordsForKey - not needed)
    ' Initialize these Variant arrays with
    ' the effort values.
    ' Use this technique instead of READ DATA
    ' which QBASIC has but not Visual Basic.

    ' Use a fibonnacci series to suggest how
    ' much effort a human voice takes to sing
    ' the notes in its range.
    ' The absolute value is the amount of effort,
    ' and the positive or negative sign shows the
    ' upper or lower end of the range.
    Dim I As Integer
    Dim E$

    Dim SopranoMax As Integer
    Dim AltoMax As Integer
    Dim TenorMax As Integer
    Dim BassMax As Integer

    If gRangeNormal Then ' normal
        SopranoMax = 17
        AltoMax = 15
        TenorMax = 17
        BassMax = 19
        If gRangeLowerSoprano Then
            SopranoMax = 19
            AltoMax = 17
        End If
        If gRangeLowerBass Then
            BassMax = 21
        End If
    Else
        SopranoMax = 21
        AltoMax = 23
        TenorMax = 19
        BassMax = 26
    End If

    If gRangeNormal Then ' normal
        ' Soprano range is from gS1=D3 to gS2=G4.
        gSopranoEffort = Array( _
            -8, -5, -3, -3, -2, -2, -1, _
            -1, 0, 0, 1, 1, 2, 2, _
            3, 3, 5, 8) ' 0-17 = 2*7 + 4
        ' Alto range is from gA1=A2 to gA2=C4.
        gAltoEffort = Array( _
            -8, -5, -3, -3, -2, -2, -1, _
            -1, 1, 1, 2, 2, 3, 3, _
            5, 8) ' 0-15 = 2*7 + 2
        ' Tenor range is from gT1=D2 to gT2=G3.
        gTenorEffort = Array( _
            -8, -5, -3, -3, -2, -2, -1, _
            -1, 0, 0, 1, 1, 2, 2, _
            3, 3, 5, 8) ' 0-17 = 2*7 + 4
            ' Bass range is from gB1=G1 to gB2=D3.
        gBassEffort = Array( _
            -8, -5, -3, -3, -2, -2, -1, _
            -1, 0, 0, 0, 0, 1, 1, _
            2, 2, 3, 3, 5, 8) ' 0-19 = 2*7 + 6
        If gRangeLowerSoprano Then
            ' Soprano range is
            ' from gS1=C3 to gS2=G4.
            gSopranoEffort = Array( _
                -8, -5, -3, -3, -2, -2, -1, _
                -1, 0, 0, 0, 0, 1, 1, _
                2, 2, 3, 3, 5, 8) ' 0-19 = 2*7 + 6
            ' Alto range is from gA1=G2 to gA2=C4.
            gAltoEffort = Array( _
                -8, -5, -3, -3, -2, -2, -1, _
                -1, 0, 0, 1, 1, 2, 2, _
                3, 3, 5, 8) ' 0-17 = 2*7 + 4
        End If
        If gRangeLowerBass Then
            ' Bass range is
            ' from gB1=F1 to gB2=D3.
            ' Bass range is from gB1=G1 to gB2=D3.
            gBassEffort = Array( _
                -8, -5, -3, -3, -2, -2, -1, _
                -1, 0, 0, 0, 0, 0, 0, _
                1, 1, 2, 2, 3, 3, 5, _
                8) ' 0-21 =3*7 + 1
        End If
    Else ' extended range gRangeMax
        ' Soprano range is from gS1=D3 to gS2=G4.
        gSopranoEffort = Array( _
            -8, -5, -3, -3, -2, -2, -1, _
            -1, -1, 0, 0, 0, 0, 1, _
            1, 1, 2, 2, 3, 3, 5, _
            8) ' 0-21 = 3*7 + 1
        ' Alto range is from gA1=A2 to gA2=C4.
        gAltoEffort = Array( _
            -8, -5, -3, -3, -2, -2, -1, _
            -1, -1, -1, 0, 0, 0, 0, _
            1, 1, 1, 1, 2, 2, 3, _
            3, 5, 8) ' 0-23 = 3*7+3
        ' Tenor range is from gT1=D2 to gT2=G3.
        gTenorEffort = Array( _
            -8, -5, -3, -3, -2, -2, -1, _
            -1, 0, 0, 0, 0, 1, 1, _
            2, 2, 3, 3, 5, 8) ' 0-19 = 2*7+6
            ' Bass range is from gB1=G1 to gB2=D3.
        gBassEffort = Array( _
            -8, -5, -3, -3, -2, -2, -1, _
            -1, -1, -1, -1, 0, 0, 0, _
            0, 0, 1, 1, 1, 1, 1, _
            2, 2, 3, 3, 5, 8) ' 0-26 = 3*7+6
    End If

    ' The voice ranges are specified in sub
    ' InitVoiceRange and used in sub ScanTriads.

    ' Note: gS2 = gRange(SOPRANO).Top
    ' and   gS1 = gRange(SOPRANO).Bot

    If SopranoMax <> gS2 - gS1 Then
        GoTo Err1
    Else
        For I = 0 To SopranoMax
            gEffort(SOPRANO, I) = gSopranoEffort(I)
        Next I
    End If

    If AltoMax <> gA2 - gA1 Then
        GoTo Err2
    Else
        For I = 0 To AltoMax
            gEffort(ALTO, I) = gAltoEffort(I)
        Next I
    End If

    If TenorMax <> gT2 - gT1 Then
        GoTo Err3
    Else
        For I = 0 To TenorMax
            gEffort(TENOR, I) = gTenorEffort(I)
        Next I
    End If

    If BassMax <> gB2 - gB1 Then
        GoTo Err4
    Else
        For I = 0 To BassMax
            gEffort(BASS, I) = gBassEffort(I)
        Next I
    End If

    gbEffortSetup = True
    Exit Sub

Err1:
    E$ = "InitVoiceEffort: Soprano "
    'Debug.Print "InitVoiceEffort: Soprano ";
    GoTo Errs
Err2:
    E$ = "InitVoiceEffort: Alto "
    'Debug.Print "InitVoiceEffort: Alto ";
    GoTo Errs
Err3:
    E$ = "InitVoiceEffort: Tenor "
    'Debug.Print "InitVoiceEffort: Tenor ";
    GoTo Errs
Err4:
    E$ = "InitVoiceEffort: Bass "
    'Debug.Print "InitVoiceEffort: Bass ";

Errs:
    E$ = E$ & _
      "range does not match sub " _
      & "InitVoiceRange in song no=" _
      & gSongLast
    txtPrint.Text = E$
    'Debug.Print _
    '    "range does not match sub InitVoiceRange."
    gInputErrs = gInputErrs + 1
    gbEffortSetup = False
    'Stop
End Sub

Private Sub StoreOption(gTextLine As String)
    ' called by sub StoreNotes
    ' Globals used: gField(1)
    ' Globals set:

    Dim E$

    Select Case gField(1)
        Case "INCLUDE"
            Call StoreOptionInclude(gTextLine)
        Case "EXCLUDE"
            Call StoreOptionInclude(gTextLine)
        Case "FIGBASS"
            Call StoreOptionFigBass(gTextLine)
        Case "RANGE"
            Call StoreOptionRange(gTextLine)
        Case "ALLOW"
            Call StoreOptionAllow(gTextLine)
        Case "ERROR"
            Call StoreOptionAllow(gTextLine)
        Case "RAISEHS"
            Call StoreOptionRaiseHS(gTextLine)
        Case "MISC"
            Call StoreOptionMisc(gTextLine)
        Case Else
            GoSub Err1
    End Select
    Exit Sub

Err1:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOption:E1 option=" & gField(1) & _
        " not yet implemented in option line=" _
        & gTextLine & " in song no=" & gSongLast
    'txtPrint.Text = E$
    Debug.Print E$
    Call DisplayText(E$)
    Return
End Sub

Private Sub StoreOptionInclude(gTextLine As String)
    ' called by sub StoreOption
    ' Globals used: gFieldLast, gField(Field)
    ' Globals set:
    '   gTriadAllowed(J)=True
    '   gSong(gSongLast).bTriadAllowed(Field) =

    ' Store the options input on the OP: line.
    ' All fields on a card must be separated by
    ' a TAB character, CHR(9).
    
    ' More than one OP: INCLUDE or OP: EXCLUDE
    ' card can be input and the changes are cumulative.
    ' INCLUDE and EXCLUDE cards can be mixed.
    
    ' The changes take effect at any place in a song
    ' for the rest of the song and for any later songs
    ' until changed by another INCLUDE or EXCLUDE card.
    
    ' Any figured bass input on a FB: card take effect
    ' and override any OP: INCLUDE or OP: EXCLUDE card.
    
    ' (1) The CHORD7TH option allows or prevents use
    ' of any chord with a dominant 7th (^7) note.
    '   OP: INCLUDE CHORD7TH
    '   OP: EXCLUDE CHORD7TH
        
    ' (2) The MAJOR7TH option allows or prevents use
    ' of any chord with a major triad-major 7th note,
    ' eg I7 is C E G B in key C:
    ' eg IV7 is G B D F in key C:
    '   OP: INCLUDE CHORD7TH
    '   OP: EXCLUDE CHORD7TH
        
    ' (3) The INCLUDE or EXCLUDE option specifies
    ' the Roman numerals allowed or excluded
    ' as triad numbers in this song.  E.g.

    ' The subfields ALL, NONE, and ONLY should
    ' come immediately after INCLUDE or EXCLUDE.

    ' To include all triads, e.g. in debugging, use
    '   OP: INCLUDE ALL     or
    '   OP: EXCLUDE NONE    or
    '   OP: EXCLUDE ONLY
    
    ' To exclude all triads, use
    '   OP: INCLUDE NONE    or
    '   OP: INCLUDE ONLY    or
    '   OP: EXCLUDE ALL
    
    ' Prefix an triad name with a hyphen to
    ' reverse its treatment.
    
    ' E.g. to include a triad to the current list,
    ' that is, to un-exclude it, use
    '   OP: INCLUDE V   or
    '   OP: EXCLUDE -V
    
    ' E.g. to exclude a triad from the current list,
    ' that is, to un-include it, use
    '   OP: INCLUDE -vi     or
    '   OP: EXCLUDE vi
    
    ' E.g. to include all triads except iii and vi,
    ' that is, to exclude only iii and vi, use
    '   OP: INCLUDE ALL -iii -vi    or
    '   OP: EXCLUDE NONE iii vi     or
    '   OP: EXCLUDE ONLY iii vi
    
    ' E.g. to exclude all triads except I and V,
    ' that is, to use only I and V, use
    '   OP: INCLUDE none I V    or
    '   OP: INCLUDE only I V
    '   OP: EXCLUDE all -V
        
    Dim A$
    Dim B$
    Dim Field As Integer
    Dim J As Integer
    Dim Triad As Variant
    Dim E$
    Dim bAllow As Boolean
    Dim bFound As Boolean
    
    ' TODO include lower case romnum's for minor control(?)
    Triad = Array("", "I", "II", "III", "IV", _
                "V", "VI", "VII")

'**********************************************
'**********************************************

Main:
    GoSub Init
    GoSub MainLine
    Exit Sub

'**********************************************
'**********************************************
    
Init:
    ' Skip over any empty fields afer gField(1),
    ' caused by two or more tab chars,
    ' in order to find any "CHORD7TH" field.
    Field = 1
    A$ = ""
    While A$ = "" And Field < gFieldLast
        Field = Field + 1
        A$ = Trim$(UCase$(gField(Field)))
    Wend
    Return
    
'**********************************************
    
MainLine:
    Select Case A$
        Case "CHORD7TH"
            GoSub Chord7th
        Case "MAJOR7TH"
            GoSub Major7th
        Case Else ' INCLUDE, EXCLUDE
            If gField(1) = "INCLUDE" _
            Or gField(1) = "EXCLUDE" Then
                GoSub IncludeExclude
            Else
                GoSub Err1
            End If
    End Select
    Return
    
'**********************************************
    
    ' This CHORD7TH option can be turned on or
    ' off at any point in reading in the Notes.
    ' Each Note will have the value set on or off.
    ' OP: INCLUDE CHORD7TH ' allow the 7th degree
    '   of a chord e.g. the F in G B D F
    ' OP: EXCLUDE CHORD7TH ' disallow the 7th
    '   degree of a chord, e.g. in medieval modes.
Chord7th:
    If gField(1) = "INCLUDE" Then
        gbSeventhExcluded = False
    Else ' gField(1) = "EXCLUDE"
        gbSeventhExcluded = True
    End If
    Return

'**********************************************
    
    ' OP: INCLUDE MAJOR7TH ' allow the 7th degree
    '   of a chord e.g. the F in G B D F
    ' OP: EXCLUDE MAJOR7TH ' disallow the 7th
    '   degree of a chord, e.g. in medieval modes.
Major7th:
    If gField(1) = "INCLUDE" Then
        gbMajorSeventhExcluded = False
    Else ' gField(1) = "EXCLUDE"
        gbMajorSeventhExcluded = True
    End If
    Return

'**********************************************
    
' sub Init4Part initialized gTriadAllowed() = True
' Values continue until changed by another input
' INCLUDE or EXCLUDE card in a song or between songs.
IncludeExclude:
    Select Case gField(1)
        Case "INCLUDE"
            For Field = 2 To gFieldLast
                A$ = Trim$(UCase$(gField(Field)))
                If A$ = "ALL" Then
                    GoSub AllowAll ' all triads are ok
                ElseIf A$ = "NONE" Or A$ = "ONLY" Then
                    GoSub AllowNone ' no triads are ok
                ElseIf A$ <> "" Then
                    If Left$(A$, 1) = "-" Then
                        bAllow = False
                        B$ = Trim$(Mid$(A$, 2))
                    Else
                        bAllow = True
                        B$ = A$
                    End If
                    GoSub FindTriad
                End If
            Next Field
            'GoSub CopyTriads
        Case "EXCLUDE"
            For Field = 2 To gFieldLast
                A$ = Trim$(UCase$(gField(Field)))
                If A$ = "ALL" Then
                    GoSub AllowNone ' no triads are ok
                ElseIf A$ = "NONE" Or A$ = "ONLY" Then
                    GoSub AllowAll ' all triads are ok
                ElseIf A$ <> "" Then
                    If Left$(A$, 1) = "-" Then
                        bAllow = True
                        B$ = Trim$(Mid$(A$, 2))
                    Else
                        bAllow = False
                        B$ = A$
                    End If
                    GoSub FindTriad
                End If
            Next Field
            'GoSub CopyTriads
        Case Else
            GoSub Err1
    End Select
    Return
'**********************************************
    
FindTriad:
    bFound = False
    For J = 1 To TRIADALLOWEDMAX ' =7
        If B$ = Triad(J) Then
            bFound = True
            If bAllow Then
                gTriadAllowed(J) = True
            Else
                gTriadAllowed(J) = False
            End If
            Exit For
        End If
    Next J
    If Not bFound Then
        GoSub Err2
    End If
    Return
    
'**********************************************
    
AllowAll:
    For J = 1 To TRIADALLOWEDMAX
        gTriadAllowed(J) = True
    Next J
    Return

'**********************************************
    
AllowNone:
    For J = 1 To TRIADALLOWEDMAX
        gTriadAllowed(J) = False
    Next J
    Return

'**********************************************
    
' TODO usable but not used.  Used on a note level.
'CopyTriads:
    'For J = 1 To TRIADALLOWEDMAX
    '    gSong(gSongLast).bTriadAllowed(J) = _
    '        gTriadAllowed(J)
    'Next J
    'Return

'**********************************************
    
Err1:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionInclude:E1 option=" _
        & gField(1) & _
        " not yet implemented in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

'**********************************************
    
Err2:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionInclude:E2 option=" _
        & gField(1) _
        & " subfield=" & A$ _
        & " is not triad I-VII in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

End Sub



Private Sub StoreOptionFigBass(gTextLine As String)
    ' called by sub StoreOption
    ' Globals used: gFieldLast, gField(Field)
    ' Globals set:
    '   gbFigBassAllowed(J)=True
    '   gNote(gNoteX).bFigBassAllowed(Field) =

    ' Store the options input on the OP: line.
    ' All fields on a card must be separated by
    ' a TAB character, CHR(9).
    
    ' The FIGBASS option specifies the Roman
    ' numerals allowed as figbass numbers in this
    ' song.
    
    ' More than one OP: FIGBASS card can be input
    ' and the changes are cumulative.
    
    ' The changes take effect on a song basis
    ' for the current and any later songs
    ' until changed by another FIGBASS card. 8may08
    
    ' Any figured bass input on a FB: card take effect
    ' and override any OP: FIGBASS card.
    
    ' The subfields ALL, NONE, and ONLY should
    ' come immediately after FIGBASS.
    
    ' To include all figbass's, e.g. in debugging, use
    '   OP: FIGBASS ALL
    
    ' To exclude all figbass's, use
    '   OP: FIGBASS NONE    or
    '   OP: FIGBASS ONLY
    
    ' Prefix a figbass name with a hyphen to
    ' reverse its treatment.
    ' E.g. to include a figbass to the current list, use
    '   OP: FIGBASS 53
    
    ' E.g. to exclude a figbass from the current list, use
    '   OP: FIGBASS -53
    
    ' E.g. to include all figbass's except 65 and 43, use
    '   OP: FIGBASS ALL -65 -43
    
    ' E.g. to exclude all figbass's except 53, use
    '   OP: FIGBASS none 53     or
    '   OP: FIGBASS only 53
        
    Dim A$
    Dim B$
    Dim Field As Integer
    Dim J As Integer
    Dim FigBass As Variant
    Dim E$
    Dim bAllow As Boolean
    Dim bFound As Boolean
    
    ' Notice that on an input FB: card, the
    ' figured bass fields have a blank separating
    ' the digits, e.g. "5 3".
    ' The blanks are not used here for convenience
    ' in processing the option card using sub Parse.
    FigBass = Array("53", "6", "64", "7", _
                "65", "43", "42")

'**********************************************
'**********************************************

Main:
    GoSub MainLine
    Exit Sub

'**********************************************
'**********************************************
    
' sub Init4Part initialized gbFigBassAllowed() = True
MainLine:
    For Field = 2 To gFieldLast
        A$ = Trim$(UCase$(gField(Field)))
        If A$ = "ALL" Then
            GoSub AllowAll ' all figbass's are ok
        ElseIf A$ = "NONE" Or A$ = "ONLY" Then
            GoSub AllowNone ' no figbass's are ok
        ElseIf A$ <> "" Then
            If Left$(A$, 1) = "-" Then
                bAllow = False
                B$ = Trim$(Mid$(A$, 2))
            Else
                bAllow = True
                B$ = A$
            End If
            GoSub FindFigBass
        Else ' ignore any blank fields
        End If
    Next Field
    'For J = 0 To FIGBASSALLOWEDMAX
    '    Debug.Print gbFigBassAllowed(J);
    'Next J
    GoSub CopyFigBassAllowed
    Return
'**********************************************
    
FindFigBass:
    bFound = False
    For J = 0 To FIGBASSALLOWEDMAX ' 0 to 6
        If B$ = FigBass(J) Then
            bFound = True
            If bAllow Then
                gbFigBassAllowed(J) = True
            Else
                gbFigBassAllowed(J) = False
            End If
            Exit For
        End If
    Next J
    If Not bFound Then
        GoSub Err2
    End If
    Return
    
'**********************************************
    
AllowAll:
    For J = 1 To FIGBASSALLOWEDMAX
        gbFigBassAllowed(J) = True
    Next J
    Return

'**********************************************
    
AllowNone:
    For J = 1 To FIGBASSALLOWEDMAX
        gbFigBassAllowed(J) = False
    Next J
    Return

'**********************************************

' TODO usable but may want to check on a note level.
CopyFigBassAllowed: ' 7may08
    For J = 0 To FIGBASSALLOWEDMAX
        gSong(gSongLast).bFigBassAllowed(J) = _
            gbFigBassAllowed(J)
    Next J
    Return

'**********************************************
        
Err1:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionFigBass:E1 option=" _
        & gField(1) & _
        " not yet implemented in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

'**********************************************
    
Err2:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionFigBass:E2 option=" _
        & gField(1) _
        & " subfield=" & A$ _
        & " is not figbass I-VII in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

End Sub

Private Sub StoreOptionMisc(gTextLine As String)
    ' called by sub StoreOption
    ' Globals used: gFieldLast, gField(I)
    ' Globals set:
    '   gTriadAllowed(J)=True
    '   gSong(gSongLast).bTriadAllowed(I) =

    ' Store the options input on the OP: line.
    ' The MAXNEXT n option specifies the number
    ' of notes to backtrack when trying to get a
    ' solution allowing errors with the
    '       OP: ALLOW xxx, e.g. X7TH
    ' n=2 is probably sufficient, and n=3 is
    ' reasonably efficient, but n=4 may take up
    ' to 100000 gNumGoBacks.

    ' 1. Case OP: MISC MAXNEXT 2 (2=dafault)
    ' where each field is separated by tab.
    ' The last line read
    ' takes effect and nullifies any previous
    ' for the same subfield, e.g. MAXNEXT.

    ' 2. Case OP: MISC SMOOTH ON
    ' OP: MISC SMOOTH OFF
    ' If SMOOTH, accept any harmonic progression
    ' where no voice moves more than a whole step
    ' as calculated by fcnbMaxSATBVarianceOver
    ' (Const LIMIT=2), to the next chord
    ' but ignore the case of identical chords.
    ' Justification: Ottman p 177 Bach example
    ' of vi64 - viio6 without a stationary 4th
    ' above the bass.

    ' 3. Case "RANKOVERVARIANCE" VarOper
    ' If gbRankOverVariance, then first the
    ' candidates with the lowest Rank is
    ' found, and then the candidate with
    ' the lowest Variance for that Rank.
    ' If there is a tie for Variance,
    ' then the operator < or <= from
    '   VarOper=LE
    '   VarOper=LT
    ' decides
    ' whether the lower numbered chord
    ' or the higher of the two is used.
    ' The rounding value
    '   VarRound=.5
    ' is not used or accepted.
    
    ' Defaults are:
    '   gbRankOverVariance = FALSE
    '   gbRankLE = False
    '   gbVarianceLE = False
    ' which is the same as:
    '   "gVarianceOverRank" is TRUE
    '   gbRankLT = true
    '   gbVarianceLT = true

    ' 4. Case "VARIANCEOVERRANK" RankOper
    ' Similar logic as for Case "RANKOVERVARIANCE"
    '   RankOper=LE
    '   RankOper=LT
    ' The rounding value
    '   VarRound=.5
    ' is used to treat cand's with almost equal
    ' variances as equal, so that they will
    ' be chosen by rank instead.

    ' 5. Case "BASSGAP" n=12 or 24 usually
    ' Force the gap between the Tenor and Bass
    ' to be less than or equal to BASSGAP.
    ' It is easier to play the harmony chords
    ' on a piano is there is only an octave
    ' between Tenor and Bass.
    
    ' 6. Case "ABCINPUT" ON or OFF
    ' Sub InputLines will read in 4Part control
    ' cards followed by the data for the four
    ' Voices in ABCPlus format.  Sub ParseABC
    ' will convert the Voice data to 4Part format,
    ' and will automatically create the TM:
    ' and the T1:-T4: lines.
    ' The ABCPlus data can be created by the abcmidi
    ' program from mymusic.MID files.
    
    ' 7. Case "NHTINPUT" ON or OFF
    ' Default is NHTONPUT ON if ABCINPUT ON.
    ' In the ABC Voice data, if a "partial chord"
    ' note has less than four tones, then flag
    '   gNote(gNoteX).xNHT = NHTHANG
    ' as NHTHANG.  Flag the "full chord" of four
    ' input tones before it and flag
    '   gNote(gNoteX).xNHT = NHTFIRST
    ' These flags are used in Sub Harmonize to
    ' add parentheses around the note heads of
    ' any tone that does not belong to the
    ' preceding or following chord.

    ' 8. Case "MAINVOICE"
    '   OP: MISC MAINVOICE voice
    ' where voice is one of
    '   SOPRANO,ALTO,TENOR,BASS
    ' Even if all four voices are input, if the
    ' MATCHVOICE specifies e.g. SOPRANO, then
    ' display all the candidates as if only
    ' the Soprano Voice had been input.
    ' (This shows all the choices that the
    ' composer faced.)
    ' TODO
    ' Should I provide for one to three mainvoices?

    ' 9. OP: MISC CONTRAR .5 OBLIQUE .7 PARALLE .9 STATION 1.0
    ' For this note, in sub ChoosePrevChord,
    ' choose candidates whose outer
    ' voices (sop and bass) have one of these four
    ' motions: contrary, oblique, parallel or stationary.
    ' For each note, get a random number from 0.0 to 1.0.
    ' Eg. if the RN is between 0 and .5, use contrary.
    ' If the RN is between .5 and .7, use oblique.
        
    ' 10. OP: MISC    RAISE67 ONEVOICE
    '     OP: MISC    RAISE67 ANYVOICE
    ' In a minor (melodic) key, the 6th and 7th
    ' scale degree, #^6 and #^7, is usually raised
    ' a half step by FB: #3, or by the program
    ' in sub fcnMinorProgression, if in the following
    ' chord #^7 is followed by the tonic or if
    ' #^6 is followed by #^7.  This OP: parm
    ' controls whether both notes are in the
    ' same voice (ONEVOICE) or in any voice
    ' (ANYVOICE).  Ottman is not clear in pg 347.
    ' See gbRaise67OneVoice.
    
    ' 11. OP: MISC    BARBEG m
    '     OP: MISC    BAREND n
    ' where m and n are <=0 and <= BARSMAX,
    ' and n >= m.
    ' This is a way to choose a range of
    ' measures from BARBEG to BAREND for
    ' testing. 20sep08
    ' I think this applies to ABCINPUT. 2dec08
    
    ' 12. OP: MISC  TENOR   OCTAVES +2
    '     OP: MISC  BASS    OCTAVES +2
    ' E.g. add 2 octaves to the pitch of
    ' each Tenor (or Bass) note in
    ' sub Note2Abc.
    
    ' 13. OP:   MISC    HARMONY SHOW    or
    '     OP:   MISC    HARMONY ON
    '     OP:   MISC    HARMONY OFF
    ' If HARMONY OFF, then show input notes only
    ' but do not do HARMONY calculations.
    
    ' 14A. OP:   MISC    COMMENT VOICES  V2 V3 V4
    ' Comment out one or more input
    ' voices n in Vn: by skipping that gTextLine
    ' in sub StoreNotes.
    
    ' 14B. OP:   MISC    COMMENT TIMES  T1  T2  T3
    ' Comment out one or more input
    ' times n in Tn: by skipping that gTextLine
    ' in sub StoreNotes.
    
    ' 14C. OP:   MISC    COMMENT LYRICS 11  21  31
    ' OR   OP:   MISC    COMMENT LYRICS 11  12  13
    ' Comment out one or more input
    ' voices n in 1n: or n1: by skipping that gTextLine
    ' in sub StoreNotes.
    
    ' 15. OP: MISC ALTO3 ON
    '     OP: MISC ALTO3 OFF
    ' If ON, then Sub WriteAbcNotes will
    ' print the Tenor line in the Alto Clef, where
    ' Middle C is on the third line from the bottom
    ' of the Alto staff.
    
    ' 16. OP: MISC BARSPERLINE 2
    ' Use this option to specify how any measures
    ' or bars to output per line.
    '   Sub InterleaveABCLines will
    ' interleave a comment line and nine lines,
    ' as follows:
    '   comment, TM, T1, V1, T2, V2, T3, V3, T4, V4
    '   The result is that the input ABC lines are output
    ' in the form of 4Part input lines, stacked
    ' vertically in short lines, for use in a new job.
    '   The number after BARSPERLINE must be >= 0 and < 16.
    ' 0 means no limit to the numbers of bars per line.
    
    ' 17. OP: MISC WRITE ROW4 % default is BASSROW4
    ' Sub WriteABCNotes will
    ' write out ROW4 instead of BASSROW4, and will
    ' not write out the harmonic analysis.
    ' This is to prepare a score for performers.
    
    ' 18. OP: MISC COPYCONTROLS VOICE1 % default=Voice0
    ' If VOICE1 to VOICE4, copy rests "z",
    ' ties "-" and slurs "(" ")" to all the
    ' other voices of the chord as if
    ' z* -* (* or *) were entered on that VOICE.
    ' This is handy when capturing VOICE1 notes from
    ' Noteworthy Composer.
    
    ' 19. OP: MISC GOBACKS MAX=nn MOD=mm
    '       % default MAX=1000 MOD=100
    ' gNumGoBacks is the number of times that
    ' sub Harmonize has to backtrack to a
    ' higher note number due to no solution.
    ' 4Part can loop when backtrack so a limit
    ' MAX is needed.  An error message is
    ' printed every MOD times.
    
'*************************************

Dim OP$
Dim A$
Dim B$
Dim C$
Dim D$
Dim I As Integer
Dim J As Integer
Dim V As Integer
Dim E$
    
    ' Used in Noterec Motion().
Const MOFF As Integer = 0
Const MCONTRARY As Integer = 1
Const MOBLIQUE As Integer = 2
Const MSIMILAR As Integer = 3
Const MSTATIONARY As Integer = 4

' OP:   gField(1) gField(2) gField(3) gField(4)
' E.G.
' OP:   MISC      MAXNEXT   n
'       OP$       A$        B$        C$

'****************************************

Main:
    GoSub MainLoop
    Exit Sub
    
'****************************************

MainLoop:
    OP$ = Trim$(UCase$(gField(1)))
    If OP$ = "MISC" Then
        A$ = Trim$(UCase$(gField(2)))
        Select Case A$
        Case "MAXNEXT"
            V = Val(gField(3))
            If V > 0 And V < 5 Then
                gMaxNextBest = V
            Else
                GoSub Err1
            End If
        Case "SMOOTH"
            B$ = Trim$(UCase$(gField(3)))
            Select Case B$
                Case "ON"
                    gbSmoothOption = True
                Case "OFF"
                    gbSmoothOption = False
                Case Else
                    GoSub Err2
            End Select
        Case "RANKOVERVARIANCE"
            gbRankOverVariance = True
            B$ = gField(3)
            GoSub GetVarOper
            B$ = gField(4)
            If B$ <> "" Then
                GoSub GetVarRound ' field 4 not accepted
            End If
        Case "VARIANCEOVERRANK"
            gbRankOverVariance = False
            B$ = gField(3)
            GoSub GetRankOper
            B$ = gField(4)
            GoSub GetVarRound
        Case "MINVAR4DUPES"
            B$ = gField(3)
            GoSub GetMinVar4Dupes
        Case "BASSGAP"
            V = Val(gField(3))
            If V > 0 And V <= BASSGAPMAX Then
                gBassGap = V
            Else
                GoSub Err5
            End If
        Case "BARBEG"
            V = Val(gField(3))
            If V >= 0 And V <= BARSMAX Then
                gBarBeg = V
            Else
                GoSub Err16
            End If
        Case "BAREND"
            V = Val(gField(3))
            If V >= 0 And V <= BARSMAX Then
                gBarEnd = V
            Else
                GoSub Err16
            End If
        Case "ABCINPUT"
            gbAbcInputOption = True
            B$ = Trim$(UCase$(gField(3)))
            Select Case B$
                Case "ON"
                    gbAbcInputOption = True
                Case "OFF"
                    gbAbcInputOption = False
                Case Else
                    GoSub Err6
            End Select
        Case "NHTINPUT"
            gbNHTInputOption = True
            B$ = Trim$(UCase$(gField(3)))
            Select Case B$
                Case "ON"
                    gbNHTInputOption = True
                Case "OFF"
                    gbNHTInputOption = False
                Case Else
                    GoSub Err8
            End Select
        Case "MAINVOICE"
            B$ = Trim$(UCase$(gField(3)))
            Select Case B$
                Case "SOPRANO"
                    gMainVoice = SOPRANO
                Case "ALTO"
                    gMainVoice = ALTO
                Case "TENOR"
                    gMainVoice = TENOR
                Case "BASS"
                    gMainVoice = BASS
                Case Else
                    GoSub Err7
            End Select
        Case "MOTION"
            For I = 3 To 6
                B$ = Trim$(UCase$(gField(I)))
                Select Case B$
                    Case ""
                        Exit For
                    Case "OFF"
                        gxMotionRequest = 0
                    Case "CONTRAR"
                        gxMotionRequest = _
                        gxMotionRequest Or CONTRARY
                    Case "OBLIQUE"
                        gxMotionRequest = _
                        gxMotionRequest Or OBLIQUE
                    Case "SIMILAR"
                        gxMotionRequest = _
                        gxMotionRequest Or Similar
                    Case "STATION"
                        gxMotionRequest = _
                        gxMotionRequest Or STATIONARY
                    Case Else
                        GoSub Err11
                End Select
            Next I
            If gSongLast > 0 Then
                gSong(gSongLast).xMotionRequest = _
                    gxMotionRequest
            End If
        Case "RAISE67"
            B$ = Trim$(UCase$(gField(3)))
            C$ = Trim$(UCase$(gField(4)))
            If B$ = "VOICE" Then
                Select Case C$
                    Case "SAME", "ONE"
                        gbRaise67OneVoice = True
                    Case "ANY", "ALL"  ' default=0
                        gbRaise67OneVoice = False
                    Case Else
                        GoSub Err13
                End Select
            ElseIf B$ = "CLOSE" Then
                Select Case C$
                    Case "TRUE", "ON"
                        gbRaise67Close = True
                    Case "FALSE", "OFF"  ' default=0
                        gbRaise67Close = False
                    Case Else
                        GoSub Err14
                End Select
            Else
                GoSub Err12
            End If
        Case "HARMONY"
            gbShowInputNotesOnly = True
            B$ = Trim$(UCase$(gField(3)))
            Select Case B$
                Case "ON", "SHOW"
                    gbShowInputNotesOnly = False
                Case "OFF"
                    gbShowInputNotesOnly = True
                Case Else
                    GoSub Err17
            End Select
        Case "COMMENT"
            B$ = Trim$(UCase$(gField(3)))
            C$ = ""
            For I = 4 To gFieldLast
                C$ = C$ & " " & Trim$(UCase$(gField(I)))
            Next I
            Select Case B$
                Case "VOICES"
                    gCommentVoices = C$
                Case "TIMES"
                    gCommentTimes = C$
                Case "LYRICS"
                    gCommentLyrics = C$
                Case Else
                    GoSub Err18
            End Select
        Case "ALTO3"
            gbAlto3 = False
            B$ = Trim$(UCase$(gField(3)))
            Select Case B$
                Case "ON"
                    gbAlto3 = True
                Case "OFF"
                    gbAlto3 = False
                Case Else
                    GoSub Err19
            End Select
        Case "BARSPERLINE"
            V = Val(gField(3))
            If V >= 0 And V < 16 Then
                gBarsPerLine = V
            Else
                GoSub Err20
            End If
        Case "WRITE"
            B$ = Trim$(UCase$(gField(3)))
            Select Case B$
                Case "ROW4"
                    gbWriteRow4 = True
                Case "BASSROW4" ' default
                    gbWriteRow4 = False
                Case Else
                    GoSub Err21
            End Select
        Case "COPYCONTROLS"
            gCopyControlsVoice = 0
            B$ = Trim$(UCase$(gField(3)))
            If Left$(B$, 5) <> "VOICE" Then
                GoSub Err22
            Else ' VOICE1 to VOICE4 16dec08
                V = Val(Right$(B$, 1))
                If V >= SOPRANO And V <= BASS Then
                    gCopyControlsVoice = V
                Else
                    GoSub Err22
                End If
            End If
        Case "GOBACKS"
            For I = 3 To 4
                B$ = Trim$(UCase$(gField(I)))
                If Mid$(B$, 4, 1) = "=" Then
                    C$ = Left$(B$, 3)
                    D$ = Mid$(B$, 5)
                    J = Val(D$)
                    If J <= 0 Then
                        GoSub Err23
                    Else
                        Select Case C$
                            Case ""
                                Exit For
                            Case "MAX"
                                gNumGoBacksMax = J
                            Case "MOD"
                                gNumGoBacksMod = J
                            Case Else
                                GoSub Err23
                        End Select
                    End If
                End If
            Next I
        Case Else
            GoSub Err0
        End Select
    End If
    Return

'*******************************************

' Get the operator (< or <=) to use when
' comparing Variances of two candidates with
' equal Rank.
' If <, keep the current cand.
' If <=, take the new cand.
GetVarOper:
    C$ = Trim$(UCase$(B$))
    If C$ = "" Then
        Return
    Else
        If C$ = "VAROPER=LE" Then
            gbVarianceLE = True
        ElseIf C$ = "VAROPER=LT" Then
            gbVarianceLE = False
        Else
            GoSub Err3
        End If
    End If
    Return

'*******************************************

' Get the operator (< or <=) to use when
' comparing Ranks of two candidates with
' equal Variance.
' If <, keep the current cand.
' If <=, take the new cand.
GetRankOper:
    C$ = Trim$(UCase$(B$))
    If C$ = "" Then
        Return
    Else
        If C$ = "RANKOPER=LE" Then
            gbRankLE = True
        ElseIf C$ = "RANKOPER=LT" Then
            gbRankLE = False
        Else
            GoSub Err4
        End If
    End If
    Return

'*******************************************

' Get the rounding value to use when
' comparing the Variances of two candidates.
' e.g. VarRound=.5
GetVarRound:
    If gbRankOverVariance Then
        GoSub Err10 ' not useful or accepted
    Else
        C$ = Trim$(UCase$(B$))
        If C$ = "" Then
            Return
        Else
            If Left$(C$, 9) = "VARROUND=" Then
                C$ = Mid$(C$, 10)
                If IsNumeric(C$) Then
                    gVarRound = Val(C$)
                Else
                    GoSub Err9
                End If
            End If
        End If
    End If
    Return

'*******************************************

    ' If this cand for gNoteX has the same scale
    ' degree step as gAfterX, e.g. V-V or v-V,
    ' flag it as a .bDupe in sub CalcVariance.
    ' If the variance between them is less than
    '   gMinVarianceForDupes
    ' then add 20 to the .Variance in sub CalcVariance,
    ' and 20 to the .Rank in sub ChoosePrevChord
    ' to discourage but not prevent its use. 18mar08
GetMinVar4Dupes:
    C$ = Trim$(UCase$(B$))
    If C$ = "" Then
        Return
    Else
        If IsNumeric(C$) Then
            gMinVarianceForDupes = Val(C$)
            If gMinVarianceForDupes < 0# _
            Or gMinVarianceForDupes > 20# Then
                GoSub Err15
            End If
        Else
            GoSub Err15
        End If
    End If
    Return
    
'*******************************************
    
Err0:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E0  invalid field 3 " _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err1:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E1  MAXNEXT value=" _
        & V & " is not between 1 and 4" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err2:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E2  SMOOTH value=" _
        & B$ & " must be ON or OFF" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err3:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E3  RankOverVariance value=" _
        & B$ & " must be VarOper=LE or LT" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err4:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E4  VarianceOverRank value=" _
        & C$ & " must be RankOper=LE or LT" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err5:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E5  BassGap value=" _
        & V & " is not between 1 and" _
        & BASSGAPMAX & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err6:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E6  ABCINPUT value=" _
        & B$ & " must be ON or OFF" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err7:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E7  MAINVOICE value=" _
        & B$ & " must be one of" _
        & " soprano,alto,tenor or bass" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err8:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E8  NHT value=" _
        & B$ & " must be ON or OFF" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err9:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E9  the value " _
        & C$ & " is not numeric in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err10:
    'gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E10  sub field VarRound= " _
        & " is not accepted in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err11:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E11  MOTION value=" _
        & B$ & " must be" _
        & " CONTRAR, OBLIQUE, SIMILAR or STATION" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err12:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E12  RAISE67 value=" _
        & B$ _
        & " must be ONEVOICE or CLOSE" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err13:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E13  RAISE67 value=" _
        & B$ _
        & " must be SAME or ONE, ANY or ALL" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err14:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E14  RAISE67 value=" _
        & B$ _
        & " must be TRUE or ON, FALSE or OFF" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err15:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E15  the value " _
        & C$ & " is not numeric " _
        & "or is not in the range 0. to 20. " _
        & "in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err16:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E16 " _
        & "gBarBeg or gBarEnd value=" _
        & V & " is not between 0 and" _
        & BARSMAX & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err17:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E17  HARMONY value=" _
        & B$ & " must be SHOW, ON or OFF" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err18:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E18  COMMENT value=" _
        & B$ & " must be VOICES, TIMES OR LYRICS" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err19:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E19  ALTO3 value=" _
        & B$ & " must be ON or OFF" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err20:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E20  BARSPERLINE value=" _
        & V & " is not between 0 (no limit) and 16" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err21:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E21 WRITE value=" _
        & B$ & " must be ROW4 or BASSROW4" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err22:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E22 COPYCONTROLS value=" _
        & B$ & " must be VOICE1 to VOICE4" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

Err23:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionMisc:E23 GOBACKS value=" _
        & B$ & " must be MAX=nn or MOD=NN" _
        & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return

End Sub

Private Sub StoreOptionRaiseHS(gTextLine As String)
    ' called by sub StoreOption
    ' Globals used: gField(1)
    ' Globals set: gRangeType =
    '   gSong(gSongLast).RangeType =

    ' The RaiseHS option is used to raise
    ' the input notes of
    ' one or all voices by half steps.
    ' OP:   RAISEHS ALL 7       (or)
    ' OP:   RAISEHS V1  -5

    Dim I As Integer
    Dim A$
    Dim B$
    Dim HS As Integer ' half steps
    Dim E$

    gbRaised = False
    A$ = Trim$(UCase$(gField(2)))
    B$ = Trim$(UCase$(gField(3)))
    HS = Val(B$)
    ' TODO 11/28/6 why are the limits -15, +15 ?
    If HS < -15 Then
        GoTo Err1
    ElseIf HS > 15 Then
        GoTo Err1
    End If
    Select Case A$
        Case "ALL"
            For I = SOPRANO To BASS
                gRaiseInput(I) = HS
            Next I
            gbRaised = True
        Case "V1"
            gRaiseInput(SOPRANO) = HS
            gbRaised = True
        Case "V2"
            gRaiseInput(ALTO) = HS
            gbRaised = True
        Case "V3"
            gRaiseInput(TENOR) = HS
            gbRaised = True
        Case "V4"
            gRaiseInput(BASS) = HS
            gbRaised = True
        Case Else
            GoTo Err1
    End Select
    Exit Sub

Err1:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionRaiseHS:E1 " & _
        " invalid option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
End Sub
Private Sub StoreOptionRange(gTextLine As String)
    ' called by sub StoreOption
    ' Globals used: gField(1)
    ' Globals set: gRangeType =
    '   gSong(gSongLast).RangeType =

    ' The RANGE option specifies extensions
    ' to the range of particular voice that
    ' is preset in the program. E.g.
    ' There is a tab keystroke delimiter after
    ' each field.
    ' OP:(tab)RANGE(tab)SOPRANOetc
    ' OP:(tab)RANGE(tab)SOPRANO(tab)BEST)(tab)etc

    ' NOTE if the BEST option is used, it must
    ' follow the normal RANGE card for that voice.
    ' The RANGE card will set the RANGE BEST option
    ' also.

    ' Note: C5 (two C's above middle C) is
    '   currently the highest note in my scales.
    ' You can flag certain ranges as BEST or
    ' VBEST, and the program will (eventually)
    ' try to avoid them except at "extremes".

    ' I tend to think that the lowest string
    ' for the cello (and double bass) is not BEST
    ' to use because it is thick, and that the
    ' instruments sound better on the higher
    ' three strings, if not the middle two.
    ' This may be true for the
    ' violin and the viola also, also beginning
    ' violin students do not like the strident
    ' highest or "E" string.

    ' True ranges of string instruments:
    ' OP: RANGE SOPRANO G2-C5 % VIOLIN
    ' OP: RANGE SOPRANO BEST D3-C5
    ' OP: RANGE ALTO    C2-F4 % VIOLA
    ' OP: RANGE ALTO    BEST G2-D4
    ' OP: RANGE TENOR   C1-F3 % CELLO
    ' OP: RANGE TENOR   BEST G2-D4
    ' OP: RANGE BASS    E0 B2 % DBL BASS
    ' OP: RANGE BASS    BEST A0-B1

    ' Useful ranges of string instruments,
    ' all in first position (extended by
    ' a half step up):
    ' for this harmony program 4Part:
    ' OP: RANGE SOPRANO G2-C5     % VIOLIN 1
    ' OP: RANGE ALTO    G2-C5     % vIOLIN 2
    ' OP: RANGE TENOR   C2-D#4    % VIOLA
    ' OP: RANGE BASS    C1-D#3    % CELLO
    ' If the Double Bass plays the Cello part one
    ' octave lower, then the Cello cannot go below
    ' E1 because the dbl bass cannot go below E0.
    ' OP: RANGE BASS    E1-D#3 % CELLO

    ' Useful ranges of string instruments
    ' with BEST ranges
    ' for this harmony program 4Part:
    ' OP: RANGE SOPRANO G2-C5 % VIOLIN 1
    ' OP: RANGE SOPRANO BEST G2-C#2 C5-C5
    ' OP: RANGE ALTO    G3-B4 % VIOLIN 2
    ' OP: RANGE ALTO    BEST G2-C#2 -
    ' OP: RANGE TENOR   C2-F4 % VIOLA
    ' OP: RANGE TENOR   BEST C2-F#2 E4-F4
    ' OP: RANGE BASS    C1-F3 % CELLO
    ' OP: RANGE BASS    BEST C1-F#1 E3-F3
    ' If the Double Bass plays the Cello part one
    ' octave lower, then the Cello cannot go below
    ' E1 because the dbl bass cannot go below E0.
    ' OP: RANGE BASS    E1-F3 % CELLO
    ' OP: RANGE BASS    BEST E1-F#1 E3-F3

' Note a fixed string will always stay a fixed
' string no matter what it is copied to, and trim
' not work on it.  You must convert a fixed string
' to an ordinary string variable (of varying
' length) in order to use trim on it.

' You cannot compare a fixed string of all
' blanks with a variable string of all blanks.
' You have to define an empty sting of all blanks
' to do the comparison.  See Empty3.

    Dim A$
    Dim B$
    Dim C$
    Dim E$
    Dim Bot$
    Dim Top$
    Dim I As Integer
    Dim Empty3 As String * 3

    A$ = Trim$(UCase$(gField(2))) ' SOPRANO/ALTO/etc
    B$ = Trim$(UCase$(gField(3))) ' G3-B4 or BEST
    C$ = Trim$(UCase$(gField(4))) ' G3-B4 or %COMMENT
    If B$ <> "BEST" Then
        C$ = B$
    End If
    GoSub Parse

    Select Case A$
        Case "SOPRANO"
            I = SOPRANO
            GoSub SetRange
        Case "ALTO"
            I = ALTO
            GoSub SetRange
        Case "TENOR"
            I = TENOR
            GoSub SetRange
        Case "BASS"
            I = BASS
            GoSub SetRange
        Case Else
    End Select
    Exit Sub

Parse:
    I = InStr(C$, "-")
    If I = 0 _
    Or I - 1 = 0 _
    Or I + 1 > Len(C$) Then
        GoTo Err2
    Else
        Bot$ = Trim$(Left$(C$, I - 1))
        Top$ = Trim$(Mid$(C$, I + 1))
    End If
    Return

SetRange:
    With gSong(gSongLast)
        If B$ = "BEST" Then
            .RANGE(I).BestBot = Bot$
            .RANGE(I).BestTop = Top$
        Else
            .RANGE(I).Bot = Bot$
            .RANGE(I).Top = Top$
            'If .RANGE(I).BestBot = Empty3 Then
                .RANGE(I).BestBot = Bot$
            'End If
            'If .RANGE(I).BestTop = Empty3 Then
                .RANGE(I).BestTop = Top$
            'End If
        End If
        ' propagate changed range values to next gSong
        gRangeDefault(I).Top = .RANGE(I).Top
        gRangeDefault(I).Bot = .RANGE(I).Bot
        gRangeDefault(I).BestTop = .RANGE(I).BestTop
        gRangeDefault(I).BestBot = .RANGE(I).BestBot
    End With
    Return

Err1:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionRange:E1 option=" & C$ & _
        " not yet implemented in option line=" _
        & gTextLine & " in song no=" & gSongLast
    DisplayText (E$)
    Debug.Print E$
    Stop
    Exit Sub

Err2:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionRange:E2 invalid syntax or " & _
        "missing hyphen '-' in field=" & _
        C$ & " in option line=" _
        & gTextLine & " in song no=" & gSongLast
    DisplayText (E$)
    Debug.Print E$
    Stop

End Sub


Private Sub StoreOptionAllow(gTextLine As String)
    ' called by sub StoreOption
    ' Globals used: gFieldLast, gField(I)
    ' Globals set: gxErrorAllowed

    ' Store the options input on the
    '   OP: ALLOW
    '   OP: ERROR
    ' cards.
    
    ' Options cards can come before the first song or
    ' in any song.
    ' You can use one or more Options cards to
    ' turn on or turn off one or more error types
    ' for a song.
    ' You cannot change options between notes.
    
    ' The options set are propagated from song to
    ' song in sub PropagateOptions
    ' until they are changed in a later card
    ' for the current or song.
    
    
    ' Use upper or lower case in the option cards.
    ' Each field is separated by a single tab,
    ' NOT BLANKS.
    ' The ALLOW option specifies the error types
    ' allowed in this song.  E.g.
    ' The ERROR option treats the error types
    ' specified as errors.
    
    ' LOGIC NOTE
    ' If the program cannot find a non-error
    ' solution for some note, it backtracks
    ' one or two notes and tries the next
    ' solution there.  If all tries are exhausted,
    ' the program will use a leading parenthesis
    ' to flag candidates that have an error
    ' which is allowed.  E.g. if you use
    '   OP: ALLOW sim4
    ' after the fourth pass of backtracking,
    ' the program marks each sim4 candidate
    ' as (SIM4 and adds 20 to its normal rank
    ' and allows it to be selected normally.
    ' See sub fcnErrorAllow, lines
    '   If gbErrorFourthPass _
    '   Or gNote(pNoteX).bMustAllowError Then
    ' This lets the program to try to find all
    ' non-error solutions by backtracking one
    ' or two notes before selecting a note with
    ' an allowed error.
    
    ' To allow all error types, e.g. in debugging, use
    '   OP: ALLOW ALL   or
    '   OP: ERROR NONE
    
    ' To dis-allow all error types, use
    '   OP: ALLOW NONE   or
    '   OP: ERROR ALL
    
    ' Prefix an error name with a hyphen to
    ' reverse its treatment.
    ' E.g. to allow an error, use
    '   OP: ALLOW P8   or
    '   OP: ERROR -P8
    
    ' E.g. to dis-allow an error, use
    '   OP: ALLOW -P8   or
    '   OP: ERROR P8
    
    ' E.g. to allow all errors except P5 and P8, use
    '   OP: ALLOW ALL -P8 -P5   or
    '   OP: ERROR NONE P8 P5
    
    ' E.g. to dis-allow all errors except sim4, use
    '   OP: ALLOW none sim4   or
    '   OP: ERROR all -sim4
    
'************************************

    ' TODO provide for severity level options
    ' as a coding convenience.

    ' severity 1: (worst)
    ' "P5","P8"
    ' severity 2:
    ' ,"Hid8","Hid5",
    ' ,"Prog63","ProgNv","PrgAny","NotFin"
    ' severity 3:
    ' ,"Sim4","BxT","TxA","AxS"
    ' severity 4:
    ' ,"Invr","Not7th" (requested in input by user)
    ' ,"User" (requested on screen by user)
    ' ,"2Inv"

    ' pErr=
    ' "P5","P8","Sim4","BxT","TxA","AxS",
    ' "Hid8","Hid5",
    ' ( "~#3","~b3","NoSol" )

    ' Err=
    ' ""NotFin","ProgNv",
    ' "Invr","Prog63","PrgAny"
    ' ( "NoMch",Not7th", "2Inv","NoSol" )

    ' .Error =
    ' gCand(gCandLast).Error = Err

    ' gCand(.BestCand).Error = "No sol"
    ' gCand(I).Error2 = "Final"
    '   ( 'gCand(I).Error = "Var=0" )
    ' If gCand(I1).Error = "~b3"
    ' gCand(SaveBestCand).Error = "Soln" or "^Soln"

    ' Err2 =
    ' If Err2 = "Final" Then

    Dim A$
    Dim I As Integer
    Dim J As Integer
    Dim K As Long
    Dim NotK As Long
    Dim L As Integer
    Dim E$
    Dim bAllow As Boolean
    Dim bPass1 As Boolean

'********************************************
'********************************************
Main:
    GoSub Select1
    Exit Sub

'********************************************
'********************************************
    
' Case ERROR is the opposite of case ALLOW.
' Case ERROR is not really needed, since
' everything can be done with case ALLOW.
' I leave Case ERROR in for now, since it was
' used in some songs. 11/20/6
Select1:
    Select Case gField(1)
        Case "ALLOW"
            bPass1 = False
            'gxErrorAllowed = 0 ' moved to Init4Part
            For I = 2 To gFieldLast
                A$ = UCase$(Trim$(gField(I)))
                If A$ = "ALL" Then ' all errors are ok
                    gxErrorAllowed = -1
                ElseIf A$ = "NONE" Then
                    gxErrorAllowed = 0 ' no errors are ok
                ElseIf A$ = "PASS1" Then
                    bPass1 = True
                Else
                    If Left$(A$, 1) = "-" Then
                        bAllow = False
                        A$ = Trim$(Mid$(A$, 2))
                   Else
                        bAllow = True
                    End If
                    GoSub FindErrorName
                End If
            Next I
            If gSongLast > 0 Then
                gSong(gSongLast).xErrorAllowed = _
                    gxErrorAllowed
                gSong(gSongLast).xErrorAllowedPass1 = _
                    gxErrorAllowedPass1
            Else
                gSong(gSongLast + 1).xErrorAllowed = _
                    gxErrorAllowed
                gSong(gSongLast + 1).xErrorAllowedPass1 = _
                    gxErrorAllowedPass1
            End If
        Case "ERROR"
            ' ERROR NONE or "any string"
            ' allows all error types
            gxErrorAllowed = -1 ' allow all
            For I = 2 To gFieldLast
                A$ = Trim$(UCase$(gField(I)))
                If A$ = "NONE" Then ' all errors are ok
                    gxErrorAllowed = -1
                ElseIf A$ = "ALL" Then
                    gxErrorAllowed = 0 ' no errors are ok
                ElseIf A$ = "PASS1" Then
                    bPass1 = True
                Else
                    If Left$(A$, 1) = "-" Then
                        bAllow = True
                        A$ = Trim$(Mid$(A$, 2))
                    Else
                        bAllow = False
                    End If
                    GoSub FindErrorName
                End If
            Next I
            If gSongLast > 0 Then
                gSong(gSongLast).xErrorAllowed = _
                    gxErrorAllowed
                gSong(gSongLast).xErrorAllowedPass1 = _
                    gxErrorAllowedPass1
            Else
                gSong(gSongLast + 1).xErrorAllowed = _
                    gxErrorAllowed
                gSong(gSongLast + 1).xErrorAllowedPass1 = _
                    gxErrorAllowedPass1
            End If
        Case Else
            GoSub Err1
    End Select
    Return

'********************************************

FindErrorName:
    For J = 1 To ERRNAMEMAX
        L = Len(gErrName(J))
        If Left$(A$, L) = gErrName(J) _
        Then
            GoSub SetBit
            Exit For
        End If
    Next J
    Return
    
'********************************************

    ' The value of bAllow in "ERROR" is the
    ' reverse of bAllow in "ALLOW".
    ' If an error is allowed by the user in Pass1,
    ' then allow it in Pass4 also, due to the code
    ' in function fcnErrAllow.
SetBit:
    K = 2 ^ (J - 1)
    If bAllow Then ' allow both Pass1 and Pass4
        ' Set this bit=1
        gxErrorAllowed = _
            gxErrorAllowed Or K
        If bPass1 Then
            gxErrorAllowedPass1 = _
                gxErrorAllowedPass1 Or K
        End If
    Else ' dis-allow both Pass1 and Pass4
        ' Set this bit=0
        NotK = Not K
        gxErrorAllowed = _
            gxErrorAllowed And NotK
        If bPass1 Then
            gxErrorAllowedPass1 = _
                gxErrorAllowedPass1 And NotK
        End If
    End If
    Return

'********************************************

Err1:
    gInputErrs = gInputErrs + 1
    E$ = "StoreOptionAllow:E1 option=" _
        & gField(1) & _
        " not yet implemented in option line=" _
        & gTextLine & " in song no=" & gSongLast
    Call DisplayText(E$)
    Debug.Print E$
    Return
End Sub

Private Sub StoreVoiceVn(pVoice As Integer, _
                    pLast As Integer, _
                    gTextLine As String)
    ' called by sub StoreNotes
    ' calls function fcnPack
    ' calls function fcnToIx

    ' pVoice = SOPRANO/ALTO/TENOR/BASS = 1/2/3/4
    ' pLast = .LastV1/.LastV2/.LastV3/.LastV4,
    ' which is the last/highest note used
    ' in array gNote for this voice.

    ' A note can be a REST, indicate by Z or z
    ' with it's length given by the TM: line.
    ' The Z or z is passed to the abcm2ps prog.
    ' Z* or z* means to extend the Rest to the
    ' other voices.

    ' "-" means TIE this note to the next note if
    ' it is the same pitch.
    ' "-*" means TIE this note to the next
    ' for all 4 voices.  E.g. G4-   G2
    
    ' An open paren "(" indicates the beginning
    ' of a slur or tie.  It can stand alone or
    ' precede a note, e.g. "(" or "(G#3".

    ' A closed paren ")" indicates the end
    ' of a slur or tie.  It can stand alone or
    ' follow a note, e.g. ")" or "G#3)".
    ' The ( or ) is passed to the abcm2ps prog.
    ' (* means to extend the Tie or Slur to the
    ' other voices.

' See Gosub CheckRandomVn and StoreRandomVn.
' The random Vn option can be useful
' when inputting a Bach
' chorale where each Vn: has all its notes
' on one line, and a key modulation is needed,
' or parens need to be added for slurs or ties.
' If a Vn: field is of the format
'       Vn: Ndddd=   C4>(C4
' eg    V3: N115=    C4>(C4
' where N may be upper or lower case
' and ddd is optional number of digits 1-4
' then set Note=dddd-1
' On the next pass of the loop, the next
' Vn: field, e.g.
'   C4>(C4
' means that (C4 will replace C4 (if C4 matches
' the input note name) will be stored at
' Note=dddd equal to Note=dddd on the TM: card.
' Any prefix and/or suffix can be used around C4.

'************************************************
    
    Dim A$
    Dim B$
    Dim C$
    Dim D$
    Dim D2$
    Dim SaveD$
    Dim Prefix$
    Dim Suffix1$
    Dim Suffix2$
    Dim Suffix$
    Dim TieLoc As Integer
    Dim ESlurLoc As Integer
    Dim Acc$
    Dim Letter1$
    Dim Digit1$
    Dim HoldDigit$

    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim V As Integer
    Dim Field As Integer
    Dim Note As Integer
    Dim NotePrev As Integer
    Dim BumpNoteTrace As Integer
    Dim BumpCount As Integer
    Dim bErr As Boolean
    Dim bAccidentalFirst As Boolean
    Dim IndexIncr As Integer
    Dim bNatural As Boolean
    Dim bDoubleSharp As Boolean
    Dim DoubleSharpIncr As Integer
    Dim Disp$
    Dim bRandomVnFound As Boolean
    Dim bRandomFix As Boolean
    Dim Loc$
    Dim Fix1$
    Dim NoteBeg As Integer
    Dim NoteEnd As Integer
    Dim LocDig As Integer
    Dim PreNote$
    Dim PostNote$
    Dim NoteName$
    Dim NoteParens$
    Dim bNextI As Boolean

'********************************************
'********************************************

Main:
    GoSub Init
    GoSub MainLoop
            'GoSub SlurTie
            'fcnPack
            'GoSub Store1
    GoSub Conclusion
    Exit Sub

'********************************************
'********************************************

Init:
   'If pLast = 10 Then
   '    I=I ' testing
   'End If
    If pVoice = 1 Then
        If Note = 47 Then
            I = I ' testing
        End If
    End If
    Note = pLast ' e.g. .LastV1 for Soprano
    Return

'*******************************************

Conclusion:
    pLast = Note ' store in .LastVx
    Return

'********************************************
'********************************************

Test1B:
    If gSongLast = 1 Then
        'If pVoice = ALTO Then
        'If pVoice = SOPRANO Then
            'If Note + 1 = 59 Or A$ = "N59=" Then
            'If Left$(A$, 1) = Chr$(34) Then
            If Left$(A$, 1) = "=" Then
                I = I
            End If
            If Note = 47 Then
                I = I
            End If
            If Field = 27 Then
                I = I
            End If
        'End If
    End If
    Return

'********************************************
' Beg of BUMPNOTE
'********************************************

' When ABC notation notes are not input by
'   OP: MISC    ABCINPUT    ON
' but "hanging" notes are input by
'   OP: MISC    NHTINPUT    ON
' then Loop and add one to Note while
'   .Time.Offset < 0
' in order to keep the input notes verticaly aligned. 27jun08
BumpNoteNo:
    Do
        GoSub BumpNoteNo2
        If bErr Then
            Return
        End If
        If gbAbcInputOption Then '27jun08
            Exit Do
        End If
    'Loop While gNote(Note).Voice(pVoice).Time.Pos < 0
    Loop While _
        gNote(Note).Voice(pVoice).Time.Offset < 0
    'GoSub Test3
    Return
    
'********************************************

Test3:
    If Note = 40 And pVoice > 1 Then
        I = I ' testing
    End If
    Return
    
'********************************************

BumpNoteNo2:
    If Note >= SONGNOTEMAX Then
        GoSub Err1 ' bErr = True
    ElseIf bRandomVnFound Then
        Note = Note + 1
        GoSub SetSongKeyForNote ' 18jul08
    ElseIf Note >= gSong(gSongLast).LastUsed(TM) Then
    'ElseIf Note > gSong(gSongLast).LastUsed(TM) Then
        GoSub Debug2
        BumpCount = BumpCount + 1
        'If BumpCount > 3000 Then ' 100=some large number
        If BumpCount > 10 Then ' 100=some large number
            GoSub Debug3
            Stop
        End If
        I = I ' testing
    Else
        Note = Note + 1
        GoSub SetSongKeyForNote ' 18jul08
    End If
    Return

'********************************************

Debug2:
    Debug.Print "StoreVoiceVn:D2 BumpNoteTrace="; _
        BumpNoteTrace
    Return
    
'********************************************

Debug3:
    Debug.Print "StoreVoiceVn:D3 "; _
    "TM: card must before Vn: in song="; _
    gSongLast
    Return
    
'********************************************

Test1:
    If Left$(A$, 2) = "(*" Then
        I = I
    End If
    Return
    
'********************************************
' End of BUMPNOTE
'********************************************

' When ABC notation notes are input by
'   OP: MISC    ABCINPUT    ON
'   OP: MISC    NHTINPUT    ON
' usually placed just before
'   KY: A MAJOR
' then sub ParseAbc will line up all the notes in
' the four voices vertically and insert blank notes
' or tabs in the Vn: lines that are then passed
' to the normal sub StoreVoiceVn here.
' In the case of blank notes:
' (1) just add one to Note;
' (2) notice that the number of notes in each Vn: line
' will equal the number of fields in the TM: line,
' that is: gSong(gSongLast).LastUsed(TM). 27jun08

MainLoop:
    bRandomVnFound = False
    For Field = 1 To gFieldLast
        A$ = gField(Field) ' check for random fix
        'GoSub Test1
        GoSub Test1B
        GoSub CheckBlank
        If bErr Then
            GoTo NextI
        End If
        BumpNoteTrace = 1: GoSub BumpNoteNo ' 17jul08
        
        GoSub CheckQuotedField ' e.g. "F"G4
        
        GoSub CheckRandom ' e.g. Nddd=,#C5.  Gosub Upcase1
        If bRandomFix Then
            GoTo NextI
        End If
        
        'BumpNoteTrace = 1: GoSub BumpNoteNo ' 17jul08
        GoSub CheckBar
        If bErr Then
            GoTo NextExit
        End If
        
        ' Uppercases A$ except for flat bA4, and z rests.
        GoSub InitBar ' Gosub Upcase1.  Set .Name = A$
        If bNextI Then
            GoTo NextI
        End If

        GoSub CheckRest
        If bNextI Then
            GoTo NextI
        End If

        GoSub ParseNote
        
        'GoSub Test1B
        GoSub SlurTieAsterisks
        
        ' Remove all blanks from the string and
        ' left-adjust all chars:
        If NoteName$ = "" Then
            A$ = ""
        Else
            A$ = fcnPack(NoteName$) ' call
        End If
        gNote(Note).Voice(pVoice).Name = A$
        If A$ = "" Then
            I = I ' testing
        End If
        'If A$ = "(*" Then
        '    I = I ' testing
        'End If
        
        GoSub Store1
NextI:
    If bRandomFix Then
        Field = Field + 1 ' skip to next pair
    End If
    Next Field
NextExit:
    Return

'********************************************

CheckBlank:
        'Select Case gField(Field)
        Select Case A$
        'Case "", " ", "-"
        Case "", " "
            Note = Note + 1 '01jan09
            GoSub SetSongKeyForNote ' 18jul08
            bErr = True
            'GoTo NextI ' skip over null fields
        Case Else
            bErr = False
        End Select
        Return
    
'********************************************

' There may be more than one field enclosed
' in double quotes before the note, e.g.
'   "G""<(     )>"B4
' or more than one ABC control enclosed in "!", e.g.
'   "F"!ff!"^text"G4
' If present, I assume that the double quote or "!"
' is the first character in a$.
CheckQuotedField:
    gNote(Note).Voice(pVoice).Quoted = ""
    If InStr(A$, Chr$(34)) > 0 Then
        I = I
    End If
    B$ = Left$(A$, 1)
    While B$ = Chr$(34) Or B$ = "!"
        GoSub StoreQuotedField
        B$ = Left$(A$, 1)
    Wend
    Return
    
'********************************************

' Find the matching ending " or !,
' concatenate the string to .Quoted,
' and strip the string off the beginning of A$.
StoreQuotedField:
    With gNote(Note).Voice(pVoice)
        I = InStr(Mid$(A$, 2), B$) + 1
        If I > 1 Then
            .Quoted = .Quoted & Mid$(A$, 1, I)
            A$ = Mid$(A$, I + 1)
        End If
    End With
    Return
    
'********************************************

' After checking A$ for:
'   blanks: "" or " "
'   randoms: Ndddd=, C5>#C4
'   rests: z or z*
' parse the true note into three optional parts:
'   PreNote$ (prefix)
'   NoteName$ is a letter (A to G) with:
'       (1) an optional accidental (X # = b bb)
'       before or after the letter;
'       (2) followed by an
'       optional digit (0 to 9).
'   PostNote$ (suffix)
' PreNote$ may contain one or more of, in order:
'   "F" midi chord or "^text"
'   !f! or !anyABCsymbol!
'   beginning slur: .(*((' or .((*,
'   staccato . under the note

' PostNote$ may include:
'   one of - -* .- .-* .-*' .-*,  and/or
'   several ) )* )*))*

' (* or -* extends ( or - to all four voices.

' ABCPlus 1.1 supports the new:
'   .( or .- is a slur or tie of dashes
'   (' or -' means the arc opens downward
'   (, or -, means the arc opens upward.

ParseNote:
    PreNote$ = ""
    NoteName$ = ""
    PostNote$ = ""
    GoSub FindNote
    If NoteBeg = 0 Then ' no NoteName$ in A$
        GoSub ParsePostNote
        If Len(A$) > 2 Or A$ = "(*" Or A$ = ")*" Then
            GoSub Debug7
            I = I
        End If
        GoTo ParseNoteExit
    End If
        
    If NoteBeg > 1 Then
        PreNote$ = Left$(A$, NoteBeg - 1)
    End If
    If LocDig < Len(A$) Then
        PostNote$ = Mid$(A$, LocDig + 1)
    End If
    If Len(A$) > 2 Or A$ = "(*" Or A$ = ")*" Then
        'GoSub Debug7
        I = I
    End If
ParseNoteExit:
    Return
    
'********************************************

' Find the first .- - or ) in A$,
' then move the chars from that point into
' PostNote$, and the chars before that point
' into PreNote$, since there is no NoteName$
' letter+digit to separate them by.
ParsePostNote:
        I = InStr(A$, ".-")
        If I = 0 Then
            I = InStr(A$, "-")
        End If
        J = InStr(A$, ")")
        
        If I = 0 Then
            I = J
        ElseIf J > 0 And J < I Then
            I = J
        End If
        
        If I > 0 Then
            PreNote$ = Left$(A$, I - 1)
            PostNote$ = Mid$(A$, I)
        Else
            PreNote$ = A$
        End If
        Return
    
'********************************************

FindNote:
    NoteBeg = 0
    NoteEnd = 0
    NoteName$ = ""
    For J = 1 To Len(A$)
        B$ = Mid$(A$, J, 1)
        If B$ = "=" Then
            I = I
        End If
        K = InStr("#=bABCDEFG", B$)
        If K > 0 Then
            If NoteBeg = 0 Then
                NoteBeg = J
            End If
        ElseIf NoteBeg > 0 Then
            NoteEnd = J '
            Exit For
        End If
    Next J
    If NoteEnd > 0 Then
        GoSub FindDigit2
        NoteName$ = Mid$(A$, NoteBeg, _
                LocDig - NoteBeg + 1)
    Else
        Stop
    End If
    Return
    
'********************************************

' NoteBeg = 1 or NoteBeg > 1
FindDigit2:
    LocDig = 0
    For J = NoteEnd To Len(A$)
        B$ = Mid$(A$, J, 1)
        If B$ >= "0" And B$ <= "9" Then
            LocDig = J
            Exit For
        End If
    Next J
    If LocDig = 0 Then
        Stop
    End If
    Return
    
'********************************************
' BEGIN OF SLURS AND TIES
'********************************************

' "(" means SLUR this note with the next.
' "(*" means SLUR this note to the next
' for all 4 voices.

' The same note name may contain a
' leading slurbeg oparen "(" or "(*"
' and a trailing tie mark "-".

' After updating the .xlur bits and
' the .OpenSlurs and .CloseSlurs counts,
' delete the asterisks in (* )* and -*
' from PreNote$ and PostNote$.

' Sub DisplayNotes will display up to
' one or more slurs and one tie for
' each note on the upper grid,
' eg. (((G4  or   A4))-

' Sub WriteABCNotes will prefix each
' note with .Quoted and .PreNote and
' will suffix each note with .PostNote.
' Notice that .Quoted, .PreNote and
' .PostNote may contain any ABC controls
' besides slurs and ties, eg.
' eg. "F""^text"!pp!!<(!(.HG4 etc.
' eg. A4))-

SlurTieAsterisks:
    If InStr(PreNote$, "(") > 0 Then
        GoSub BegSlurAsterisks
    End If
    If InStr(PostNote$, ")") > 0 Then
        GoSub EndSlurAsterisks
    End If
    If InStr(PostNote$, "-") > 0 Then
        GoSub TieAsterisk
    End If
    Return
    
'********************************************
        
' NOT USED 3jan09
    With gNote(Note).Voice(pVoice)
        If Left$(.Name, 2) = "(*" Then
            .xSlur = .xSlur Or SLURBEG Or SLURALL4
            Prefix$ = Left$(.Name, 1)
            A$ = Mid$(.Name, 3)
        ElseIf Left$(.Name, 1) = "(" Then
            .xSlur = .xSlur Or SLURBEG
            Prefix$ = Left$(.Name, 1)
            A$ = Mid$(.Name, 2)
        Else
            A$ = .Name
        End If
    End With
    GoSub SlurTie
    Return
    
'********************************************
' May have a mix of several (* and ( in
' PreNote$ for this pVoice.

BegSlurAsterisks:
    With gNote(Note).Voice(pVoice)
        If pVoice = gCopyControlsVoice Then
            GoSub ChangeBegSlursToAllFours
        End If
        I = InStr(PreNote$, "(*")
        While I > 0
            .xSlur = .xSlur Or SLURALL4
            GoSub BegSlurCount
            ' Change this (* to ( and find next (*
            PreNote$ = _
             fcnRepFirst(PreNote$, "(*", "(") ' call
            I = InStr(PreNote$, "(*")
        Wend
        
        ' Count the original ('s and changed (*'s
        J = InStr(PreNote$, "(")
        If J > 0 Then
            .xSlur = .xSlur Or SLURBEG
            .OpenSlurs = .OpenSlurs + 1
            For I = J + 1 To Len(PreNote$)
                If Mid$(PreNote$, I, 1) = "(" Then
                    .OpenSlurs = .OpenSlurs + 1
                End If
            Next I
        End If
        .PreNote = PreNote$
    End With
    Return
    
'********************************************
' Use this option
'   OP: MISC COPYCONTROLS VOICE1 %default=0
' to change all ('s to (*'s in PreNote$
' if this is the specified pVoice.

ChangeBegSlursToAllFours:
    I = InStr(PreNote$, "(*")
    If I = 0 Then
        J = InStr(PreNote$, "(")
        If J > 0 Then
            PreNote$ = _
             fcnReplace(PreNote$, "(", "(*") ' call
        End If
    End If
    Return
    
'********************************************

' Update beg slur count for voices <> pVoice
BegSlurCount:
    For V = SOPRANO To BASS
        If V <> pVoice Then
            With gNote(Note).Voice(V)
                .xSlur = .xSlur Or SLURBEG
                .OpenSlurs = .OpenSlurs + 1
                .PreNote = .PreNote & "("
            End With
        End If
    Next V
    Return
    
'********************************************
' May have a mix of several )* and ) in
' PostNote$ for this pVoice.

EndSlurAsterisks:
    With gNote(Note).Voice(pVoice)
        If pVoice = gCopyControlsVoice Then
            GoSub ChangeEndSlursToAllFours
        End If
        I = InStr(PostNote$, ")*")
        While I > 0
            .xSlur = .xSlur Or SLURALL4
            GoSub EndSlurCount
            ' Change this )* to ) and find next )*
            PostNote$ = _
             fcnRepFirst(PostNote$, ")*", ")") ' call
            I = InStr(PostNote$, ")*")
        Wend
        
        ' Count the original )'s and changed )*'s
        J = InStr(PostNote$, ")")
        If J > 0 Then
            .xSlur = .xSlur Or SLUREND
            .CloseSlurs = .CloseSlurs + 1
            For I = J + 1 To Len(PostNote$)
                If Mid$(PostNote$, I, 1) = ")" Then
                    .CloseSlurs = .CloseSlurs + 1
                End If
            Next I
        End If
        .PostNote = .PostNote & PostNote$
    End With
    Return
    
'********************************************
' Use this option
'   OP: MISC COPYCONTROLS VOICE1 %default=0
' to change all )'s to )*'s in PostNote$
' if this is the specified pVoice.

ChangeEndSlursToAllFours:
    I = InStr(PostNote$, ")*")
    If I = 0 Then
        J = InStr(PostNote$, ")")
        If J > 0 Then
            PostNote$ = _
             fcnReplace(PostNote$, ")", ")*") ' call
        End If
    End If
    Return
    
'********************************************

' Update End slur count for voices <> pVoice
EndSlurCount:
    For V = SOPRANO To BASS
        If V <> pVoice Then
            With gNote(Note).Voice(V)
                .xSlur = .xSlur Or SLUREND
                .CloseSlurs = .CloseSlurs + 1
                .PostNote = .PostNote & ")"
            End With
        End If
    Next V
    Return
    
'********************************************

TieAsterisk:
    With gNote(Note).Voice(pVoice)
        I = InStr(PostNote$, "-*")
        If I > 0 Then
            .xTie = .xTie Or TIE Or TIEALL4
            GoSub TieAllFour
            PostNote$ = _
             fcnRepFirst(PostNote$, "-*", "-") ' call
        ElseIf InStr(PostNote$, "-") > 0 Then
            ' See OP: MISC COPYCONTROLS VOICE1 %default=0
            If pVoice = gCopyControlsVoice Then
                .xTie = .xTie Or TIE Or TIEALL4
                GoSub TieAllFour
            Else
                .xTie = .xTie Or TIE
            End If
        End If
        .PostNote = .PostNote & PostNote$
    End With
    Return
    
'********************************************

' Update .Tie for voices <> pVoice
TieAllFour:
    For V = SOPRANO To BASS
        If V <> pVoice Then
            With gNote(Note).Voice(V)
                .xTie = .xTie Or TIE
                .PostNote = .PostNote & "-"
            End With
        End If
    Next V
    Return
            
'********************************************
' END OF SLURS AND TIES
'********************************************

CheckBar:
    If A$ = "|" Or A$ = "||" Then
        ' Advance to the next note with a bar
        ' to match the incoming Vn: Field
        Do While Not gNote(Note).bBar
            BumpNoteTrace = 2: GoSub BumpNoteNo
            If bErr Then
                'Return
                Exit Do
            End If
        Loop
    End If
    Return
    
'********************************************

InitBar:
    With gNote(Note).Voice(pVoice)
        .xRest = RESTNONE
        .xSlur = SLURNONE
        .xTie = TIENONE
        If Not bRandomFix Then
            GoSub UpperCase1
        End If
        
        .Name = A$
        'A$ = "" ' 3jan09

        If .Name = "|" Or .Name = "||" Then
            .Index2Name = -1 ' minus means not a note
            .Effort = 0
            .ChordPos = 0
            bNextI = True
            'GoTo NextI
        Else
            bNextI = False
        End If
    End With
    Return
    
'********************************************

' Distinguish between a leading "b" flat or "bb"
' double flat and a note name "B",
' as in "b4" "bB4" "bbG4".
' Lowercase the "b" or "bb" and uppercase the rest.

' Probably only Melodic Minor ascending and descending
' in the same measure, or in a key change to a
' key with more flats.

' Notice in my 4Part notation (Stan's Notation),
' that a note is a letter A to G followed by an
' octave number 0 to 7.
UpperCase1:
    If Len(A$) <= 2 Then
        A$ = UCase$(A$)
    Else
        I = InStr(A$, "b")
        If I = 0 Then
            A$ = UCase$(A$)
        Else ' E.G. "bB4" or "(C5"
            GoSub FindDigit
            If LocDig <= 2 Then
                A$ = UCase$(A$)
            Else
                B$ = Left$(A$, LocDig - 2)
                C$ = Mid$(A$, LocDig - 1)
                A$ = LCase$(B$) & UCase$(C$)
            End If
        End If
    End If
    A$ = fcnRepChar(A$, "Z", "z") ' call
    Return
    
'********************************************

FindDigit:
    LocDig = 0
    For J = I + 1 To Len(A$)
        B$ = Mid$(A$, J, 1)
        If B$ >= "0" And B$ <= "9" Then
            LocDig = J
            Exit For
        End If
    Next J
    Return
    
'********************************************
' Notice that z rest was lowercased at label Upcase1:
' Notice that z or z* stand alone on a Vn: line.
CheckRest:
    With gNote(Note).Voice(pVoice)
        'If LCase$(Left$(.Name, 1)) = "z" Then
        If Left$(.Name, 1) <> "z" Then
            bNextI = False
        Else
        ' store and bypass "rest" note
            .Index2Name = -1 ' minus means "rest"??
            .Effort = 0
            .ChordPos = 0
            ' See OP: MISC COPYCONTROLS VOICE1 %default=0
            If pVoice = gCopyControlsVoice Then
                If .Name = "z" Then
                    .Name = "z*" ' 16dec08
                End If
            End If
            
            If .Name = "z*" Then
                .xRest = RESTALL4
                .Name = Left$(.Name, 1)
                gNote(Note).bRestAll4 = True
            Else
                .xRest = REST1
                gNote(Note).bRestAll4 = False
            End If
            'GoTo NextI
            bNextI = True
        End If
    End With
    Return
    
    ' For my mod of Arvo Part's Credo:
    'If UCase$(Left$(.Name, 1)) = "z" Then
    '    .Name = .Name & "*" ' temp for Arvo 28sep08
    'End If

    '.Name = UCase$(.Name) ' 29sep08
    '.Name = LCase$(.Name) ' 29sep08
    'If pVoice = BASS Then ' 30sep08 Arvo Credo
        '.Name = "z*"
    'Else
        '.Name = "z"
    'End If
    
'********************************************
' Beg of RANDOM
'********************************************

' E.g.  V3:   N98=    C4>(C4
CheckRandom:
    If Right$(A$, 1) = "=" Then ' eg N98=
        ' notice usually here Note=pLast
        bRandomFix = True
        GoSub UpperCase1
        Loc$ = A$
        Fix1$ = gField(Field + 1) ' eg C4>(C4
        GoSub CheckRandomVn
        I = I
    Else
        bRandomFix = False
    End If
    Return
    
'********************************************

' See comments at begin of this sub.
CheckRandomVn:
    bRandomVnFound = False ' insurance
    If UCase$(Left$(Loc$, 1)) = "N" Then ' Ndddd=
        B$ = Mid$(Loc$, 2, Len(Loc$) - 2) ' dddd
        If IsNumeric(B$) Then
            Note = Val(B$)
            bRandomVnFound = True
            GoSub StoreRandomVn
            Note = Note - 1 ' adjust for Bump later
        End If
    Else
        GoSub Err4
        Stop
    End If
    Return
    
'*******************************************

' e.g. Input note C4>(C4 means to replace C4 with (C4
' Usually the note C4 was input on a Vn: line after TM:.
StoreRandomVn:
    With gNote(Note).Voice(pVoice)
        I = InStr(Fix1$, ">")
        If I > 0 Then
            .NameFix = Fix1$ ' save info
            If .Name = Left$(Fix1$, I - 1) Then ' =old note?
                .Name = Mid$(Fix1$, I + 1) ' new input note
                A$ = .Name  ' new input note
            ElseIf .Name <> "" Then
                A$ = .Name  ' old input note
                GoSub Debug6
            End If
        End If
    End With
    Return
    
'*******************************************

' NOT USED 11aug08
' The RandomVn name, eg C4>(C4, has already
' been stored at note Ndddd=, eg note 115.
' If the input note name for this voice matches C4,
' then replace it with A$="(C4".
UpdateRandomVn:
    With gNote(Note).Voice(pVoice)
    
    I = InStr(.NameFix, ">")
        If I > 0 Then
            If A$ = Left$(.NameFix, I - 1) Then
                A$ = Mid$(A$, I + 1)
            ElseIf A$ <> "" Then
                GoSub Debug6
            End If
        End If
    End With
    Return

'********************************************
' NOT USED 11aug08
            If bRandomVnFound Then
                bRandomVnFound = False
                GoSub StoreRandomVn
                Note = pLast
                GoTo NextI
            Else
                GoSub CheckRandomVn
                If bRandomVnFound Then
                    GoTo NextI
                End If
            End If
    
'*******************************************
' End of RANDOM
'********************************************

    ' ")" means end slur for this voice.
    ' ")*" means end slur for all 4 voices.
    ' "-" means tie this note to the next,
    '   which is at the same pitch.
    ' "-*" means tie this note to the next
    '   note for all 4 voices,
    '   which are at the same pitch.

    ' There are three possible combinations
    ' of an ABC slur and tie mark:
    ' 1)  (A4- A4   B4)
    ' 2)  (A4  B4-  B4)
    ' 3)  (A4  B4)- B4)
    ' or
    ' 1)  (*A4-* A4     B4)*
    ' 2)  (*A4   B4-*   B4)*
   ' 3)  (*A4   B4)*-* B4)*
    
    ' I allow form (3) even though (2)
    ' makes more sense, because you are
    ' ending a slur after a tied pair,
    ' not in the middle of one.

    ' I do not allow this form
    ' 4)  (A4  B4-) B4)     or
    ' 4)  (*A4  B4-*)* B4)*
    ' even if ABC might.

SlurTie:
    With gNote(Note).Voice(pVoice)
        B$ = A$ & " " ' add a dummy byte at end
        ESlurLoc = InStr(B$, ")")
        Suffix1$ = ""
        If ESlurLoc > 0 Then
            .xSlur = .xSlur Or SLUREND
            If Mid$(B$, ESlurLoc, 2) = ")*" Then
                .xSlur = .xSlur Or SLURALL4
            End If
            Suffix1$ = ")"
            B$ = Left$(B$, ESlurLoc - 1)
        End If

        ' If "-*", then set TIEALL4 and then both
        ' tied notes must have the same index or pitch.
        ' See fcnbCandChord.
        C$ = A$ & " " ' add a dummy byte at end
        TieLoc = InStr(C$, "-")
        Suffix2$ = ""
        If TieLoc > 0 Then
            .xTie = .xTie Or TIE
            If Mid$(C$, TieLoc, 2) = "-*" Then
                .xTie = .xTie Or TIEALL4
            'ElseIf gbAbcInputOption Then
            'ElseIf pVoice = 1 Then
            ' See OP: MISC COPYCONTROLS VOICE1 %default=0
            ElseIf pVoice = gCopyControlsVoice Then
                .xTie = .xTie Or TIEALL4 ' 16dec08
            End If
            Suffix2$ = "-"
            C$ = Left$(C$, TieLoc - 1)
        End If
        'If Len(Suffix2$) > Len(Suffix1$) Then
        If Len(C$) <= Len(B$) Then
            A$ = C$
        Else
            A$ = B$
        End If
        A$ = Trim$(A$)
        Suffix$ = Suffix1$ & Suffix2$ ' ) or - or )-
    End With
    Return

'*******************************************

' A$ should be just letter, digit and optional
' accidental, e.g. C#4, #C4, bB4, =B4, B4, xF4
' I assume that a double sharp note occurs in
' raising #^3, #^6, or #^7 in a minor key like
' G# minor (5 sharps).

' WARNING - LIMITATION 12/15/6.
' I do not support a double flat, e.g. bbB4, or
' a double sharp, e.g. xF4, that comes from
' a unusual key of more than 7 flats or more
' 7 sharps. (Something Chopin might do but not Bach.)

' Save the octave number of the current note,
' if any, e.g. "4" in V1: E4 ....
' in HoldDigit$.  For convenience,
' you can drop the octave number of any note
' following on the same Vn: line as long
' as the octave number doesn't change.
' You only need to supply the octave number
' for a note if it changes.
Store1:
    GoSub CheckNewSongKey
    With gNote(Note).Voice(pVoice)
        GoSub Test2
        A$ = Trim$(A$)
        If A$ = "" Then
            GoTo Store1B
        End If
        
        Digit1$ = Right$(A$, 1)
        If Digit1$ >= "0" And Digit1$ <= "8" Then
            HoldDigit$ = Digit1$
        Else
            A$ = A$ + HoldDigit$
        End If
        GoSub CheckSpelling

        GoSub CheckRaiseHSOption ' new A$
        GoSub CheckRaiseHSCard ' new A$
        
        ' TODO cleanup
        If .Index2Name > gRange(pVoice).Top _
        Or .Index2Name < gRange(pVoice).Bot Then
            GoTo Err2
        Else
            .Effort = 0
           '.Effort = gEffort(pVoice, _
           '    .Index2Name - _
           '    gRange(pVoice).Bot)
        End If
        
        '.Name = Prefix$ & gNoteName(.Index2Name) _
        '    & Suffix$
        If .Index2Name >= 99 Then
            I = I
        Else
            .gName = gNoteName(.Index2Name)
            If .Name <> .gName Then
                'GoSub Debug5
            End If
        End If
Store1B:
        .Name = Prefix$ & A$ & Suffix$
        Prefix$ = ""
        Suffix$ = ""
        .ChordPos = 0
    End With
    Return
    
        ' TODO not used?
        ' (Store the effort value taken from
        ' the offset from the bottom note
        ' of this voice's range.)
    
'********************************************

Test2:
      If gSongLast = 8 Then
          'If Note = 55 Then
              'If pVoice = 1 Then
              If A$ = "=A3" Then
                  I = I
              End If
          'End If
      End If
      If A$ = "=D4" Then
          I = I
      End If
      If A$ = "" Then
          I = I ' testing
      End If
      If Left$(A$, 1) = "=" Then
          I = I ' testing
      End If
      Return
      
'********************************************

Debug5:
    Debug.Print "StoreVoiceVn:D5 Note,Voice,.Name,.gName="; _
        Note; pVoice; gNote(Note).Voice(pVoice).Name; _
        " "; gNote(Note).Voice(pVoice).gName
    I = I
    Return
    
'********************************************

' E.g. if OP: MISC RAISEHS ALL +12, then raise
' the index for all voices up by 12 half-steps.
' TODO 12/15/6 check limits

' TODO 25aug08 check bAccidentalFirst

CheckRaiseHSOption:
    With gNote(Note).Voice(pVoice)
      If gbRaised Then ' set in StoreOptionRaiseHS
          If gRaiseInput(pVoice) <> 0 Then
              .Index2Name = .Index2Name + _
                  gRaiseInput(pVoice)
              A$ = gNoteName(.Index2Name)
              If bAccidentalFirst Then
                 bAccidentalFirst = False
                 'A$ = gNoteName(.Index2Name)
                 GoSub ReverseBack
             End If
          End If
      End If
    End With
    Return
    
'********************************************

' E.g. if this card is entered:
'     HS    +7
' then raise the index for this pVoice
' up by 7 half-steps.  Same for negative vals.
' Save the value of .RaiseHS from the previous
' note, even if not a note, like a rest or bar
' (for simplicity).

' TODO 25aug08 check bAccidentalFirst

CheckRaiseHSCard:
    With gNote(Note).Voice(pVoice)
      If gNote(Note).bNewRaiseHS Then
            I = I
      Else
            I = I
      End If
      If Not gNote(Note).bNewRaiseHS Then
            NotePrev = fcnNoteBefore(Note) ' call
            If NotePrev > 0 Then
                  gNote(Note).RaiseHS = _
                  gNote(NotePrev).RaiseHS
            End If
      End If
      If gNote(Note).RaiseHS <> 0 Then
            .Index2Name = .Index2Name + _
                  gNote(Note).RaiseHS
            A$ = gNoteName(.Index2Name)
      End If
    End With
    Return
    
'********************************************

' NOT USED 25aug08
          If bAccidentalFirst Then
             bAccidentalFirst = False
             'A$ = gNoteName(.Index2Name)
             GoSub ReverseBack
         End If
    
'********************************************

' At Store1 in sub StoreVoiceVn, I try to accept
' every input note by testing alternative spellings
' with fcnToIx: (1) with or without a natural sign
' "="; (2) reversing accidental and letter; (3)
' enharmonic name for the same pitch e.g. Bb0 and A#0.
'   This allows all the input notes to be
' displayed on the upper screen.  Since each tone of
' each note is processed by sub Harmonize using
' its index into gNoteName to identify the tone
' uniquely, it is possible for Harmonize to analyze the
' notes correctly on the upper screen even if the note
' spellings do not agree with the current key for that
' note.
'    The advantage from displaying all the input
' notes is that the user can more easily inspect the
' notes and analysis on the screens to decide on the
' correct modulation key for each group of notes.  The
' screen will also display the note numbers so that the
' user can more easily enter the modulation keys in
' cards like:
'   RN: n13 = K2: n54 = K3: n63 = K1:

' TODO 28jul08 In sub StoreVoiceVn, display the
' discrepancies between the input notes and the
' expected spellings in gNoteName.  Use Debug.Print and
' messages on the upper screen, e.g. a c\Command button
' to display both names in the same note box in Rows
' 1-4.
    
CheckSpelling:
    With gNote(Note).Voice(pVoice)
        gbfcnToIxErrMsg = False ' suppress err msg
        ' Display Disp$ on screen instead of
        ' gNoteName(.Index2Name)
        Disp$ = A$
        If Len(A$) > 2 Then
            D$ = A$
            If Left$(D$, 1) = "=" Then
                ' try with "="
                .Index2Name = fcnToIx(D$) ' call
                If .Index2Name = 99 Then
                    D$ = Mid$(D$, 2) ' try without "="
                    gbfcnToIxErrMsg = True ' err msg
                    .Index2Name = fcnToIx(D$) ' call
                    If .Index2Name = 99 Then
                        GoTo Err5A ' invalid accidental
                    End If
                    D$ = A$
                End If
            ElseIf Left$(D$, 1) = "x" Then
                .Index2Name = fcnToIx(D$) ' call
                If .Index2Name = 99 Then
                    GoTo Err5B ' invalid accidental
                End If
            Else ' A#4 or #A4
                GoSub Try3CharAndReverse
            End If
        Else ' Len(A$) = 2
            ' E.g. in D Major, change input note C4 to C#4:
            D$ = fcnAddAccidentalToInput(A$) ' call
            If D$ <> A$ Then ' A#4 or #A4
                Disp$ = D$ ' disp on screen with added # or b
                GoSub Try3CharAndReverse
            Else ' A4 or =A4
                .Index2Name = fcnToIx(D$) ' call
                ' try =A in gNoteName
                If .Index2Name = 99 Then
                    gbfcnToIxErrMsg = True ' err msg
                    D$ = "=" & A$
                    .Index2Name = fcnToIx(D$) ' call
                    If .Index2Name = 99 Then
                        GoTo Err5C ' invalid accidental
                    End If
                End If
            End If
        End If
        'A$ = Disp$
        A$ = D$
        'If .Index2Name <> 99 Then
        '    If A$ <> gNoteName(.Index2Name) Then
        '        J = .Index2Name
        '        GoSub Debug4
        '        GoSub Debug1
        '        GoSub PrintNotes
        '        I = I
        '    End If
        'End If
    End With
    Return
    
'********************************************
    
' try A#, Ab, or reversed #A, bA
Try3CharAndReverse:
    With gNote(Note).Voice(pVoice)
        SaveD$ = D$
        .Index2Name = fcnToIx(D$) ' call
        If .Index2Name = 99 Then ' reverse let and acc
            Letter1$ = Left$(D$, 1)
            Acc$ = Mid$(D$, 2, 1)
            Digit1$ = Right$(D$, 1)
            D$ = Acc$ & Letter1$ & Digit1$
            .Index2Name = fcnToIx(D$) ' call
            If .Index2Name = 99 Then
                GoSub TwoCharNote
                If .Index2Name = 99 Then
                    GoTo Err5D ' invalid accidental
                Else
                    D$ = SaveD$
                    A$ = SaveD$
                End If
            End If
        End If
    End With
    Return
    
'********************************************

' If note="Bb", try the enharmonic (same pitch
' but different spelling) note="A#"
TwoCharNote:
    With gNote(Note).Voice(pVoice)
        If Acc$ = "#" Or Acc$ = "b" Then
            D2$ = Letter1$ & Acc$
        Else
            D2$ = Acc$ & Letter1$
        End If
        If fcnbEnharmonic(D2$, D$) Then ' call
            D$ = D$ & Digit1$ ' eg Bb4
            gbfcnToIxErrMsg = True ' err msg
            .Index2Name = fcnToIx(D$) ' call
        End If
    End With
    Return
    
'********************************************

' Sub StoreRN may have set gNote(Note).SongKey
'  and gSongKey (not gSongKeyVn)
' for some Note if a modulation key K2: or K3:
' or K4: was entered, or if K1: was entered
' to return to the key signature.  24jul08
SetSongKeyForNote:
    If Not gNote(Note).SongKeySet Then
        gNote(Note).SongKeySet = True
        If Note = gSong(gSongLast).FirstNote Then
            gNote(Note).SongKey = 1
        ElseIf gNote(Note).SongKey = 0 Then
            gNote(Note).SongKey = gNote(Note - 1).SongKey
        End If
    End If
    Return
    
'********************************************

' For this pVoice, if the SongKey for this note
' has changed from the last note, then
' initiate the new key. 24jul08
CheckNewSongKey:
    If Note = gSong(gSongLast).FirstNote Then
        gSongKeyVn = gNote(Note).SongKey
        GoSub GetgKeyName
        'GoSub Debug1
        Call InitChordsForKey ' 17jul08
    Else 'If Note > gSong(gSongLast).FirstNote Then
        If gNote(Note).SongKey <> gSongKeyVn Then
            gSongKeyVn = gNote(Note).SongKey
            GoSub GetgKeyName
            'GoSub Debug1
            Call InitChordsForKey ' 17jul08
        End If
    End If
    Return
    
'********************************************
    
GetgKeyName:
    I = gNote(Note).SongKey
    With gSong(gSongLast)
        ' Get gKeyName and gKeyMode from
        ' the key signature K1:, or the
        ' modulation keys K2: etc.
        ' Convert fixed string*2 to string
        ' by adding a trailing blank:
        gKeyName = Trim$ _
            (.Key(I).Name & " ")
        gKeyMode = .Key(I).Mode
    End With
    Return
    
'********************************************
    
' (1) The natural sign, "=" , is taken care of
' in sub InitHalfStepNames at AdjustNaturals:.
' E.g. if a key has four sharps, then put
' their four naturals in array gHalfStepName(),
' which is used to build array gNoteName().

' (2) The double sharp, e.g. xF4 in the key of
' G# minor (5 sharps), is taken care of in
' sub InitNeverDouble at AdjustAccidental:,
' which adds the proper note names, adjusted
' up by a half step, for #^3, #^6 and #^7,
' e.g. Bb4 to B4, F4 to #F4, F#4 to xF4.

' (3) There is usually no need to input a leading
' sharp or flat, since function
' fcnAddAccidentalToInput adds one for any
' note in the key signature.
' The exception is the use of a sharp or flat
' to reverse the effect of a natural sign,
' e.g. =B4 followed later by bB4, =C4 by #C4.
' In this case, this GoSub will reverse the
' order of the letter and accidental, e.g.
' #C4 will become C#4 so that it can be found
' in the key signature tables
'   gKeySig()
'   gSong(gSongCurr).KeySig()
' and in gNoteName by lookup function fcnToIx.
'
' Reminder - a natural sign on a note lasts only
' for that pitch and only til the end of the measure.

GetAccidental:
    Acc$ = Left$(A$, 1)
    If Acc$ = "#" Or Acc$ = "b" Then
        GoSub ReverseLetterAccid
    Else
        Acc$ = Mid$(A$, 2, 1)
        If Acc$ = "#" Or Acc$ = "b" Then
            GoSub ReverseLetterAccid
        End If
    End If
    Return

'********************************************

ReverseLetterAccid:
    bAccidentalFirst = True
    If Len(D$) <> 3 Then
        GoSub Err3
    Else
        Letter1$ = Mid$(D$, 2, 1)
        Digit1$ = Right$(D$, 1)
        If Letter1$ < "A" _
        Or Letter1$ > "G" Then
            GoSub Err3
        ElseIf Digit1$ < "1" _
        Or Digit1$ > "8" Then
            GoSub Err3
        Else
            D$ = Letter1$ & Acc$ & Digit1$
        End If
    End If
    Return
    
'********************************************
' Reorder: Let + Acc + Dig to Acc + Let + Dig
ReverseBack:
    If Len(A$) = 3 Then
        Letter1$ = Left$(A$, 1)
        Acc$ = Mid$(A$, 2, 1)
        Digit1$ = Right$(A$, 1)
        A$ = Acc$ & Letter1$ & Digit1$
    ElseIf Len(A$) <> 2 Then
        ' insurance, probably never happen
        GoSub Err4
    End If
    Return
    
'********************************************

' If a note has a natural sign, eg =B or a double
' sharp xF4 that is not in the original key
' signature, then try to get the .Index2Name
' anyway.
' The spelling of the notename may be wrong
' and the .Index2Name may not fit in any valid
' chord, but at least the program will not stop
' during the note input phase.  Later, in the
' Harmonize phase, the gSongKey will be used
' to change to a Modulation key, eg K2:.
RetryAccid:
    With gNote(Note).Voice(pVoice)
        bDoubleSharp = True
        DoubleSharpIncr = 2
        D$ = Mid$(D$, 2)
        .Index2Name = fcnToIx(D$) + DoubleSharpIncr ' call
    End With
    Return
        
' NOT USED 28 jul08
RetryAccid2:
    With gNote(Note).Voice(pVoice)
        Acc$ = Left$(D$, 1)
        bNatural = False
        If Acc$ = "=" Then
            bNatural = True
            D$ = Mid$(D$, 2)
            .Index2Name = fcnToIx(D$) ' call
        End If
    End With
    Return
        
'********************************************

Err1:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreVoiceVn:E1 LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        "reached NOTEMAX="; SONGNOTEMAX; _
        "in "; gTextLine
    Stop
    Return

'********************************************

Err2:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreVoiceVn:E2 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "voice=V " & pVoice; _
        " range="; gRange(pVoice).Bot; _
        "to"; gRange(pVoice).Top; _
        "note="; Note; " Field="; _
        Field; gField(Field); _
        " ="; gNote(Note).Voice(pVoice).Index2Name; _
        "out of range in "; gTextLine
    GoSub PrintNotes
    I = I
    Stop
    'Print gNote(Note).Voice(pVoice).Name;

'********************************************

Err3:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreVoiceVn:E3 LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        "invalid format note="; A$; _
        " should be like #C4 in "; gTextLine
    Stop
    Return

'********************************************

Err4:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreVoiceVn:E4 LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        " invalid gNoteName="; _
        A$; " for index="; _
        gNote(Note).Voice(pVoice).Index2Name; _
        " should be like F4 or F#4 in "; gTextLine
    Stop
    Return

'********************************************

Err5A: B$ = "A": GoTo Err5
Err5B: B$ = "B": GoTo Err5
Err5C: B$ = "C": GoTo Err5
Err5D: B$ = "D": GoTo Err5
Err5:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreVoiceVn:E5"; B$; " LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "voice=V " & pVoice; _
        " invalid accidental for note="; _
        Note; " Field="; _
        Field; gField(Field); _
        " index no="; gNote(Note).Voice(pVoice).Index2Name; _
        "in:"
        Debug.Print "StoreVoiceVn:E5"; B$; " gTextLine="; _
            gTextLine
    GoSub PrintNotes
    I = I
    Stop
    'Print gNote(Note).Voice(pVoice).Name;

'********************************************

PrintNotes:
    K = 0
    Debug.Print "StoreVoiceVn: Note NoteName="
    If Note > 100 Then
      J = Note - 100
    Else
      J = 1
    End If
    For I = J To Note
        Debug.Print I; _
            gNote(I).Voice(pVoice).Name;
        K = K + 1
        If K > 9 Then
            Debug.Print
            K = 0
        End If
    Next I
    Debug.Print
    Return

'********************************************

Debug1:
    Debug.Print "StoreVoiceVn:D1 Note="; Note; _
        " Voice="; pVoice; _
        " gNote(Note).SongKey="; _
        gNote(Note).SongKey; _
        " gKeyName="; gKeyName; _
        " gKeyMode="; gKeyMode
    Return
    
'********************************************

Debug4:
    Debug.Print "StoreVoiceVn:D4 LineNo="; _
        gInputLineNo; _
        " song no="; gSongLast; _
        " Note="; Note; _
        " input note="; A$; _
        " does not match gNoteName()="; _
        gNoteName(gNote(Note).Voice(pVoice).Index2Name)
    I = I
    'Stop
    Return

'********************************************

Debug6:
    Debug.Print "StoreVoiceVn:D6 LineNo="; _
        gInputLineNo; _
        " song no="; gSongLast; _
        " Note="; Note; _
        " Voice="; pVoice; _
        " input fix note="; A$; _
        " does not match input Note Name="; _
        gNote(Note).Voice(pVoice).Name
    I = I
    'Stop
    Return

'********************************************

Debug7:
    Debug.Print "StoreVoiceVn:D7 LineNo="; _
        gInputLineNo; _
        " song no="; gSongLast; _
        " Note="; Note; _
        " Voice="; pVoice; _
        " parse input note="; A$; _
        " as PreNote$="; PreNote$; _
        " NoteName$="; NoteName$; _
        " PostNote$="; PostNote$
    I = I
    'Stop
    Return


End Sub

Private Sub StoreLyricWn(pType As Integer, _
                    pLast As Integer, _
                    gTextLine As String)
    
    ' called by sub StoreNotes
    ' ByVal pType As Integer
    ' pType = W1/W2/W3/W4 = 1/2/3/4
    ' pLast = .LastUsed(11/12/13/14)
    ' which is the last/highest note used
    ' in array gNote for this voice.
    
    ' Store up to four lyric lines for Voice pType
    
    Dim I As Integer
    Dim J As Integer
    Dim Note1 As Integer
    Dim A$

'******************************************
'******************************************

Main:
    GoSub Init
    GoSub MainLoop
    Exit Sub

'******************************************

Init:
    ' Get the number n of the highest lyric line,
    ' mn:, input in this song.
    If gNumLyricLines < pType Then
        gNumLyricLines = pType ' 1,2,3,4
    End If
    Return
    
'******************************************

MainLoop:
    Note1 = pLast ' e.g. .LastUsed(W11) for Lyric1 line
    For I = 1 To gFieldLast
        If Note1 >= SONGNOTEMAX Then
            GoTo Err1
        Else
            Note1 = Note1 + 1
            While gNote(Note1).Voice(pType).Time.Offset < 0
                Note1 = Note1 + 1
                If Note1 >= SONGNOTEMAX Then
                    GoTo Err1
                End If
            Wend
            A$ = gField(I)
            If A$ = "-" Then
                A$ = ""
            End If
             ' Z* = Rest, * = skip note, _ = skip note
            If A$ = "z*" _
            Or A$ = "*" _
            Or Right$(A$, 1) = "_" Then
                Note1 = Note1 + 1
            End If
            gNote(Note1).Lyric(pType) = A$
        End If
    Next I
    'GoSub Debug1
    pLast = Note1 ' store in .LastUsed(Wn)
    Exit Sub

'******************************************

Debug1:
    Debug.Print "StoreLyricWn:D1 LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        "stored note="; Note1
    Debug.Print "D1: in gTextLine="; gTextLine
    J = Note1
    Note1 = pLast ' e.g. .LastUsed(W1) for Lyric1 line
    For I = 1 To J
        Note1 = Note1 + 1
        Debug.Print Note1; gNote(Note1).Lyric(pType);
    Next I
    Debug.Print
    Return

'******************************************

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreLyricWn:E1 LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        "reached NOTEMAX="; SONGNOTEMAX; _
        "in "; gTextLine
    Exit Sub
    'Print gNote(Note1).Voice(pType).Lyric(pType);
End Sub

Private Sub StoreLyricVoiceWn(pVoice As Integer, _
                    pLine As Integer, _
                    pLast As Integer, _
                    gTextLine As String)
    
    ' Call StoreLyricVoiceWn(VoiceLyric, LyricLine, _
    '               .LastUsed(Type1), gTextLine)
    
    ' called by sub StoreNotes
    ' ByVal pVoice As Integer
    ' pVoice = W11/W12/W13/W14 = 1/2/3/4
    ' pLast = .LastUsed(16/17/18/19)
    ' which is the last/highest note used
    ' in array gNote for this voice.
    
    ' Store up to four lyric lines for Voice pVoice
    
    ' IMPORTANT.
    ' In the input file, e.g. Bach's R.156 or 000306b.txt,
    ' after the ABC V:n lines, and any RN: lines like
    '   RN: n13=    K2:
    ' you MUST insert a terminator line in column 1
    '   E:.  or   E:
    ' in order for the Parsed T1:-T4: lines to be processed
    ' before the lyric lines for this Voice, e.g. Voice=1
    '   11: Z*  O_  |   Lord!   How man-_   y   |   mis-_
    ' else the lyric lines will not line up with the notes.
    ' 31jul08
    
    ' In sub StoreLyricVoiceWn,
    ' You can add a slur for one or all four voices by
    ' adding ( (* ) )* to a lyric word.
    ' WARNING see the new SlurTie logic in sub
    ' StoreVoiceVn.  Also need to update the slur
    ' counts in
    '   gnote(n).voice(v).openslurs
    '   gnote(n).voice(v).closeslurs
    ' 4jan09
    
    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim Note1 As Integer
    Dim A$
    Dim B$
    Dim Field As Integer
    Dim BumpId As Integer

'******************************************
'******************************************

Main:
    GoSub Init
    GoSub MainLoop
    Exit Sub

'******************************************

Init:
    ' Get the number n of the highest lyric line,
    ' mn:, input in this song.
    If gNumLyricLines < pLine Then
        gNumLyricLines = pLine ' 1,2,3,4
    End If
    Return
    
'******************************************

MainLoop:
    Note1 = pLast ' e.g. .LastUsed(W11) for Lyric1 line
    For Field = 1 To gFieldLast
        A$ = gField(Field)
        If A$ = "-" Then
            A$ = ""
        End If
        If A$ = "" Then
            GoTo Next1
        End If
        BumpId = 1: GoSub BumpNote
        If A$ = "*" Or A$ = "_" Then
            'GoSub BumpNote
        ElseIf Right$(A$, 1) = "_" Then
            B$ = Left$(A$, Len(A$) - 1)
            gNote(Note1).Voice(pVoice).Lyric(pLine) = B$
            BumpId = 2
            GoSub BumpNote ' bump again for "_" in "word_"
            A$ = Right$(A$, 1)
        End If
        If A$ <> "" Then ' 8aug08
            If A$ = "|" Then
                A$ = "" ' 1n: lines from ParseAbcNoteName
            ElseIf A$ = "z" Then
                A$ = "*" ' 1n: lines from ParseAbcNoteName
            End If
            gNote(Note1).Voice(pVoice).Lyric(pLine) = A$
            GoSub AddSlursToNote
        End If
Next1:
    Next Field
    'GoSub Debug1
    pLast = Note1 ' store in .LastUsed(mn)
    Exit Sub
    
'******************************************

' See OP: MISC COPYCONTROLS VOICE1 %default=0 16dec08
AddSlursToNote:
    With gNote(Note1).Voice(pVoice)
        If Left$(A$, 1) = "(" Then
            .xSlur = .xSlur Or SLURBEG
            If pVoice = gCopyControlsVoice Then
                .xSlur = .xSlur Or SLURALL4 ' "(*"
            End If
        ElseIf Right$(A$, 1) = ")" Then
            .xSlur = .xSlur Or SLUREND
            If pVoice = gCopyControlsVoice Then
                .xSlur = .xSlur Or SLURALL4 ' ")*"
            End If
        End If
    End With
    Return
    
'******************************************

BumpNote:
    GoSub BumpNote2
    While gNote(Note1).Voice(pVoice).Time.Offset < 0
        BumpId = 3: GoSub BumpNote2
    Wend
    Return

'******************************************
    
BumpNote2:
    Note1 = Note1 + 1
    If Note1 > SONGNOTEMAX Then
        GoSub Err1
    End If
    If Note1 > gSong(gSongLast).LastNote Then
        GoSub Err2
        I = I
    End If
    'GoSub Debug2
    Return

'******************************************

Debug1:
    Debug.Print "StoreLyricVoiceWn:D1 LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast
    Debug.Print "D1: gTextLine="; gTextLine
    Debug.Print "D1: ";
    For K = pLast To Note1
        Debug.Print K; _
            gNote(K).Voice(pVoice).Lyric(pLine);
    Next K
    Debug.Print
    Return

'******************************************

Debug2:
    Debug.Print "StoreLyricVoiceWn:D2 LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast
    With gNote(Note1).Voice(pVoice).Time
        Debug.Print "StoreLyricVoiceWn:D2 meas="; _
            gNote(Note1).MeasureNo; _
            "note,field,voice="; Note1; Field; pVoice; _
            "TRname,TRval="; .Name; .Val; _
            "TRunits,TRpos="; .Units; .Pos; _
            "TRoffset,TRdefaultpos="; .Offset; .DefaultPos
    End With
    Return

'******************************************

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreLyricVoiceWn:E1 LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        "reached SONGNOTEMAX="; SONGNOTEMAX; _
        "in "; gTextLine
    I = I
    'Stop
    Return


'******************************************

Err2:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreLyricVoiceWn:E2 LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        "reached this song's TM NOTEMAX="; _
        gSong(gSongLast).LastNote; _
        "in gTextLine:"
    Debug.Print "StoreLyricVoiceWn:E2="; _
        gTextLine
    I = I
    'Stop
    Return

End Sub

Private Sub StoreTM(pType As Integer, _
                    pLast As Integer, _
                    gTextLine As String)
    ' called by sub StoreNotes
    ' Uses global gField()

    ' Enter with
    '   pType = const TM = 1
    ' or
    '   pType = const T1 to T4 = 2 to 5
    ' Subtract 01 from the type nos 02-05 to
    ' get Voice numbers 01-04.
    
    ' pLast = .LastTM
    ' which is the last/highest note used
    ' in array gNote for this pType-1 voice.

    ' Count the number of single and double
    ' measure bars.
    ' Sub StoreSong checks for at least one
    ' double bar before the next song starts.
    ' TODO
    ' This is probably not needed because of
    ' the following error messages.

    ' First note in the TM: line of the song
    ' cannot be a SINGLEBAR | or a DOUBLEBAR ||.
    ' Last note in the TM: line of the song
    ' must be a DOUBLEBAR ||.

    ' The TM: card must be processed before
    ' any of the T1:-T4: cards, which can come
    ' in any order. The Tn: and Vn: cards are
    ' related by Voice number n.  V1: can follow
    ' anywhere after T1: or even after all the Tn:s.
    
    ' 12/11/6.
    ' The time letters on the TM: or Tn: cards
    ' can be upper or lower case, e.g. Q3 or q.
    ' Each of the valid fields on a Tn: card
    ' sets up a note and time value (Q, E, etc)
    ' for Voice n.  E.g. If T1: has 4 Q fields,
    ' then the following V1: card has 4 notes.
    
    ' The Tn: and Vn: cards can have any number
    ' of blank or hyphen fields, which are
    ' completely ignored.  The fields on the
    ' Tn: and Vn: cards can be vertically aligned
    ' or completely mis-aligned.  They must have
    ' the same number of valid fields.
    
    ' 12/8/6.
    ' I think a Tn: card can be used alone,
    ' to allow the program to choose the notes
    ' for that voice, but
    ' a Vn: card must follow a Tn: card.

' The following were copied from DECLARATIONS
' as a convenience:

' W=whole note, H=half note, Q=quarter note,
' E=eighth note, S=sixteenth note, T=thirty-second.
Const TIMENAME = _
    "WH.Q...E.......S|.||...........T"
'   "12345678901234567890123456789012"
'    00 0   0       11 1            3
'    12 4   8       67 9            2

' NoteUnits As Integer ' =WHOLENOTEUNITS / .Time

    Dim I As Integer
    Dim Note As Integer
    Dim Field As Integer
    Dim UnitsPerWNote As Integer
    Dim FirstNote As Integer
    Dim LastNote As Integer
    Dim nChords
    Dim nDoubleBars
    Dim A$
    Dim E$
    Dim TMName$
    Dim TMTime$
    Dim TMDotted$
    Dim LenDot As Integer
    Const LT As String = "<<<<<"
    Const GT As String = ">>>>>"
    Dim bErr As Boolean
    Dim Voice As Integer
    Dim SaveNote As Integer
    Dim Numer As Integer
    Dim Denom As Integer
    Dim TimeUnits As Integer
    Dim BumpTrace As Integer
    Dim bTMDebug As Boolean
    Dim bTMDebugSave As Boolean
    Dim PrevNote As Integer ' probably not needed
    Dim FieldBar As Integer ' SINGLEBAR,DOUBLEBAR

'*********************************************
'*********************************************

Main:
    'bTMDebug = True
    'bTMDebug = False
    GoSub Init
    GoSub MainLoop
        'GoSub BumpNoteNo
            'GoSub Err1
        'GoSub CheckFieldLength
            'GoSub Err3
            'GoSub EditDotted
                'GoSub Err6
            'GoSub GetNumerDenom
                'GoSub Err7
            'GoSub EditTypeTimeTM
                'GoSub UpdateNoteTimesSBarTM
                'GoSub UpdateNoteTimesDBarTM
                'GoSub UpdateNoteTimesTM
                    'GoSub AdjustTimeUnits
                    'GoSub Debug1
                    'GoSub InitNote
                    'GoSub CopyDefaults
                'GoSub Err2
            'GoSub EditTypeTime
                'GoSub MeasureTime
                    'GoSub MatchedNotes
                        'GoSub UpdateNoteTimesSBar
                        'GoSub UpdateNoteTimesDBar
                        'GoSub UpdateNoteTimes
                            'GoSub AdjustTimeUnits
                            'GoSub Debug1
                            'GoSub InitNote
                            'GoSub CopyDefaultsToTheRest
                        'GoSub Err2
                    'GoSub CheckNHT
                        'GoSub FindTheNHTCulpritTone
                    'GoSub CopyDefaultsToTheRest
                    'GoSub Debug3
                    'GoSub Err9
    Exit Sub

'*********************************************
'*********************************************

Init:
    Note = pLast
    bErr = False
    GoSub Test2
    Return

'*********************************************

Test2:
    If gSongLast = 1 Then
        'if pType - 1 = ALTO
        If pType = 5 Then
            'Debug.Print "StoreTm:D10 gTextLine=";gTextLine
            I = I
        'if Note-plast = 9
        End If
    End If
    Return
    
'*********************************************

'   For each Field on the TM: or T1:-T4: card,
' get the next Note.
'   In each measure, the number of note positions
' on a Tn: card will be equal to or less than
' the number of note positions on the TM: card.
'   The TM: card is a consolidation of all the
' note positions of the T1: to T4: cards, either
' provided by the user in the 4Part card input,
' or calculated from the ABC format input in
' ParseAbc.
'   The TM: card must be processed before any
' of the T1:-T4: cards, which can come in any
' order.
'   If this is a T1: to T4: card and if the
' number of notes for this Voice is less than
' the total number of note positions in this
' measure, then the note number will be bumped
' at
'   GoSub EditTypeTime (at a Bar)
'   GoSub MeasureTime
' until both positions are equal in the measure.

MainLoop:
    For Field = 1 To gFieldLast
        GoSub Test3
        ' Skip any dash - blank or null in a T1:-T4: card.
        ' It is only used to vertically stack the
        ' notes in the input text file. 25/03/08
        If gField(Field) = "-" _
        Or gField(Field) = " " _
        Or gField(Field) = "" Then
            ' skip
        Else
            ' For each new TM: or T1:-T4: Field,
            ' get the next note to match.
            BumpTrace = 1: GoSub BumpNoteNo
            If bErr Then
                Exit For
            End If
            GoSub CheckFieldLength
            If bErr Then
                Exit For
            End If
        End If
    Next Field

    ' Do these even if error, for possible use
    ' in debugging.
    FirstNote = pLast + 1
    LastNote = Note
    ' TODO 12/11/6 try to let
    If pType = TM Then
        pLast = Note ' index of the last note in TM: line
    End If
    Return

'*********************************************

Test3:
    If gSongLast = 3 Then
        If pType - 1 = ALTO Then
            If Field = 4 Then
                I = I ' test
                'bTMDebug = True
            Else
                'bTMDebug = False
            End If
        End If
    End If
    'GoSub SetDebug
    Return
'*********************************************

SetDebug:
    If pType >= 1 And pType <= 4 Then
        If Field >= 4 _
        And Field <= 7 _
        And Note <= 7 Then
            bTMDebug = True
            I = I ' testing
        Else
            bTMDebug = False
        End If
    Else
        bTMDebug = False
    End If
    Return

'*********************************************

BumpNoteNo:
        If Note >= SONGNOTEMAX Then
            GoSub Err1
        ElseIf pType > TM _
        And Note > gSong(gSongLast).LastUsed(TM) Then
            Debug.Print "StoreTM:D4 BumpTrace="; _
                BumpTrace; _
                "note="; Note; _
                "went past .LastUsed(TM)"
            'Stop
            I = I ' testing
            bErr = True
        Else
            PrevNote = Note ' probably not needed
            Note = Note + 1
            ' re-initialize with gNote(0) for cmdNextFile
            If pType = 1 Then ' TM:  card
                If gbFileNameNext Then
                    If gNote(Note).bUsed Then
                        gNote(Note) = gNote(0)
                    End If
                End If
                gNote(Note).bUsed = True
            End If
            
            ' Save the number of the measure in this
            ' gSongLast which this note is in,
            ' for display.
            ' TODO 30apr08 check .MeasureNo count
            If pType = TM Then
                gNote(Note).MeasureNo = _
                    gSong(gSongLast).NumSingleBars + _
                    gSong(gSongLast).NumDoubleBars + 1
                'Debug.Print "StoreTm:D11 gSongLast="; _
                '    gSongLast; _
                '   Note; gNote(Note).MeasureNo
            End If
        End If
        Return

'*********************************************

CheckFieldLength:
    TMTime$ = ""
    TMDotted = ""
    Numer = 1
    Denom = 1
    A$ = UCase$(gField(Field))
    ' tilde ~ is used for beaming in ABCPlus the
    ' following note to this one: e or s or t's.
    If Right$(A$, 1) = "~" Then ' strip off
        A$ = Left$(A$, Len(A$) - 1)
    End If
    TMName$ = Left$(A$, 1) ' E, W, etc
    'Select Case Len(gField(Field))
    Select Case Len(A$)
        Case 0
            GoSub Err3
            GoTo Return1
        Case 1
            'A$ = UCase$(gField(Field))
            TMName$ = A$
        Case Else ' Len > 1
            If InStr(A$, "|") > 0 Then ' || Bar
                TMName$ = A$
                If Left$(A$, 2) = "||" _
                And Field = 2 Then
                    'GoTo Return1 ' 18nov08
                End If
            Else
            ' TODO clean this up 12jul08
                I = InStr(A$, ">")
                If I > 0 Then
                    TMDotted$ = Mid$(A$, I)
                    TMTime$ = Mid$(A$, 2, I - 2)
                    GoSub EditDotted
                    If bErr Then
                        GoTo Return1
                    End If
                Else
                    I = InStr(A$, "<")
                    If I > 0 Then
                        TMDotted$ = Mid$(A$, I)
                        TMTime$ = Mid$(A$, 2, I - 2)
                        GoSub EditDotted
                        If bErr Then
                            GoTo Return1
                        End If
                    Else
                        TMTime$ = Mid$(A$, 2) ' time factor
                        ' eg 3/2 in E3/2
                        GoSub GetNumerDenom
                    End If
                End If
            End If
    End Select
    If pType = TM Then
        GoSub EditTypeTimeTM
    Else ' T1:-T4:
        GoSub EditTypeTime
    End If
Return1:
    Return

'*********************************************

    ' TODO convert a string of <<<< or >>>>
    ' into time durations in sub StoreTM.

    ' Allow trailing >...> or <...<
    ' for ABC dotted value,
    ' E.g. TMName=Q> or H<<.
    ' In ABC, Q>Q is dotted quarter note
    ' followed by an eighth note.
    ' E.g. H<<H is an eighth note (H/4=E)
    ' followed by a double-dotted half note.
    ' Leave the > or < following the note in
    '   gNote(Note).TMName
    ' The sub WriteABC will pass the > or < to
    ' the ABCPlus program.
EditDotted:
    LenDot = Len(TMDotted$)
    ' Edit the string of <<<< or >>>>'s.
    If LenDot > Len(LT) Then
        GoSub Err6
    ElseIf TMDotted$ = Left$(GT, LenDot) Then
        '
    ElseIf TMDotted$ <> Left$(LT, LenDot) Then
        GoSub Err6
    End If
    Return

'*********************************************

' In converting from the ABC time format to
' 4Part, the default time, e.g. E=1/8 if L: 1/8,
' is multiplied by a time factor n/d.
' E.g. Q=1/4 note becomes E2,
' a dotted Q=1/4 + 1/8 note becomes E3/2.
GetNumerDenom:
    I = InStr(TMTime$, "/")
    Select Case I
        Case 0
            Numer = Val(TMTime$)
            Denom = 1
        Case 1
            Numer = 1
            Denom = Val(TMTime$)
        Case Else
            Numer = Val(TMTime$)
            Denom = Val(Mid$(TMTime$, I + 1))
    End Select
    If Numer = 0 Or Denom = 0 Then
        GoSub Err7
    End If
    Return

'*********************************************
' BEGIN TM: Card EDIT
'*********************************************

' Edit the type of time note or bar.
EditTypeTimeTM:
    UnitsPerWNote = InStr(TIMENAME, TMName$)
    Select Case UnitsPerWNote
    Case 17
        'FieldBar = SINGLEBAR ' -1 "|" bar
        'UnitsPerWNote = WHOLENOTEUNITS ' dummy value
        GoSub UpdateNoteTimesSBarTM
        'GoSub ResetNotePos
        gNotePos(TM) = 0 ' reset for next measure
    Case 19
        'FieldBar = DOUBLEBAR ' -2 "||" bars
        'UnitsPerWNote = WHOLENOTEUNITS ' dummy value
        GoSub UpdateNoteTimesDBarTM
        'GoSub ResetNotePos
        gNotePos(TM) = 0 ' reset for next measure
    Case Is > 0
        'GoSub MeasureTime
        GoSub UpdateNoteTimesTM
    Case Else ' not found in TIMENAME string
        GoSub Err2
    End Select
    Return

'*********************************************

UpdateNoteTimesSBarTM:
    gNote(Note).TMName = TMName$
    gNote(Note).Time = SINGLEBAR ' -1 "|" bar
    gNote(Note).bBar = True
    gNote(Note).MeasTime.Val = SINGLEBAR ' "|"
    gNote(Note).MeasTime.Pos = gNotePos(TM)
    gSong(gSongLast).NumSingleBars = _
        gSong(gSongLast).NumSingleBars + 1
    Return

'*********************************************

UpdateNoteTimesDBarTM:
    gNote(Note).TMName = TMName$
    gNote(Note).Time = DOUBLEBAR ' -2 "||" bars
    gNote(Note).bBar = True
    gNote(Note).MeasTime.Val = DOUBLEBAR ' "||"
    gNote(Note).MeasTime.Pos = gNotePos(TM)
    gSong(gSongLast).NumDoubleBars = _
        gSong(gSongLast).NumDoubleBars + 1
    Return

'*********************************************

' Save the time values for this measure
'   .MeasTime.Name
'   .MeasTime.Val
'   .MeasTime.Pos = gNotePos(TM)
'   .MeasTime.Units
' in the note corresponding to the position
' on the TM: card.
'   The TM: card contains the positions in this
' measure of all possible notes from the
' four voices T1:-T4:.
'   Then update
'   gNotePos(TM)
' for the next position in the measure
' when this Gosub is entered again.

' Compare
'   gNote(Note).MeasTime.Pos
' which refers to all possible notes
' in the measure with
'   gNote(Note).Voice(pType - 1).Time.Pos
' which refers to the position of a note
' for this Voice = pType-1 in the measure.
'   Important fact:
' The number of notes for a Voice in a measure
' is always less than or equal to the number of
' notes created in a measure from the fields
' on the TM: card.  If less than, then there
' is a NHT tone in the measure for some voice.

UpdateNoteTimesTM:
    With gNote(Note)
        .TMName = UCase$(gField(Field))
        .Time = UnitsPerWNote 'eg:1=W whole note
        .MeasTime.Name = UCase$(gField(Field))
        .MeasTime.Val = UnitsPerWNote 'eg:1=W wh.note
        TimeUnits = WHOLENOTEUNITS / _
            UnitsPerWNote
        GoSub AdjustTimeUnits
        .MeasTime.Units = TimeUnits
        .MeasTime.Pos = gNotePos(TM)
        gNotePos(TM) = gNotePos(TM) + _
            TimeUnits ' position for next note
        GoSub Debug1
        GoSub InitNote
        GoSub CopyDefaults
    End With
    Return

'*********************************************

AdjustTimeUnits:
    ' E.g. if Tn: E3/2, then multiply the TimeUnits
    ' of E by 3/2.
    If Numer > 1 Then
        TimeUnits = TimeUnits * Numer
    End If
    If Denom > 1 Then
        TimeUnits = TimeUnits / Denom
    End If
    Return

'*********************************************

InitNote:
    gNote(Note).bSeventh = False
    gNote(Note).Inversion = _
        NOINVERSIONNEEDED
    gNote(Note).bSeventhExcluded = _
        gbSeventhExcluded
    For I = 1 To TRIADALLOWEDMAX
        gNote(Note).bTriadAllowed(I) = _
            gTriadAllowed(I)
    Next I
    ' Not needed yet.  Used on a song basis.  7may08
    'For I = 0 To FIGBASSALLOWEDMAX
    '    gNote(Note).bFigBassAllowed(I) = _
    '        gbFigBassAllowed(I)
    'Next I
    Return

'*********************************************

' Use the TM: card to set the defaults for the
' four Tn: card fields, for convenience in input.

'Offset As Integer ' -n means the nth note back
'DefaultPos As Integer ' .DefaultPos=.Pos is set
'   by Tn: card for any higher Tn+1: to T4: card,
'   as a convenience in input.

' WARNING:
' Do NOT do this for .Pos due to logic at
' GoSub CheckNHT:

CopyDefaults:
    If bTMDebug Then
        Debug.Print "StoreTM:D6 ptype="; pType; gTextLine
        Debug.Print "StoreTM:D6 Note; Voice; .Name; "; _
            " .Val; .Units; .Offset; .DefaultPos"
    End If
    For Voice = SOPRANO To BASS
        With gNote(Note).Voice(Voice).Time
            .Name = gNote(Note).MeasTime.Name
            .Val = gNote(Note).MeasTime.Val
            .Units = gNote(Note).MeasTime.Units
            '.Pos = gNote(Note).MeasTime.Pos ' DO
                ' NOT USE THIS 30apr08 12jul08
            .Offset = gNote(Note).MeasTime.Offset
                ' 30apr08
            'If .Offset = -4 Then
            '    I = I
            'End If
            .DefaultPos = gNote(Note).MeasTime.Pos
            If bTMDebug Then
                Debug.Print "StoreTM:D3 "; Note; Voice; _
                    .Name; " "; .Val; .Units; .Offset; .DefaultPos
            End If
        End With
    Next Voice
    If bTMDebug Then
        I = I
    End If
    Return

'*********************************************

Debug1:
    If Not bTMDebug Then Return
    'If Field < 20 Or Field > 28 Then Return
    'If Note > 123 Then
        '
    'ElseIf Note < 0 And Note > 42 Then
    'Else
    'If Note < 124 Or Note > 127 Then
    'If Note < 4 Or Note > 5 Then
    '    Return
    'End If
    With gNote(Note).MeasTime
        Debug.Print "StoreTM:D1 meas="; _
            gNote(Note).MeasureNo; _
            "note,field="; Note; Field; _
            "Mname,Mval="; .Name; .Val; _
            "Mpos,Munits="; .Pos; .Units; _
            "voice,gNotePos(pType)="; _
            pType - 1; gNotePos(pType); _
            "TM="; TM
    End With
    Return

'*********************************************
' END   TM: Card EDIT
'*********************************************
' BEGIN EDIT for this T1:-T4: Card
'*********************************************

' Edit the type of time note or bar.
' e.g. there ae 2 H's in a W note, 4 Q's in a W.
EditTypeTime:
    UnitsPerWNote = InStr(TIMENAME, TMName$)
    GoSub MeasureTime
    Return

'*********************************************

' TODO not needed?
' May need reset only for TM: and this pType(?)
ResetNotePos:
    For I = TM To T4
        gNotePos(I) = 0 ' reset to beg of measure
    Next I
    'SaveNote = 0 ' insurance, maybe for pickup note.
    Return

'*********************************************

' This logic is critical for processing
' Non-Harmonic Tone (NHT) notes,
' but it is less complicated than it sounds.

'   All the possible note positions from the
' T1: to T4: cards are combined on the
' measure-level TM: card.
'   All the possible note positions in a measure
' have been stored in
'   gnote(note).MeasTime.Pos
' in the notes gotten to match
' the fields from the TM: card, which must
' be processed first.
'   As each item is gotten from a Tn: card, its
' position in the measure must match one of the
' TM: card positions
'   gnote(note).MeasTime.Pos
' If no match, the next note is
' gotten until it matches the position calculated
' from the previous field on this Tn: card.
'   Then the position in the measure is updated
' for that voice in
'   gNote(SaveNote).Voice(pType - 1).Time.Pos
'   After each Tn: card field is placed in the
' correct note, the position of the next note
' to use from that Tn: card for that Voice=pType-1
' is updated in
'   gNotePos(pType)
' for the next time that this Gosub is entered.

MeasureTime:
    GoSub Test1
    Select Case pType
        Case T1, T2, T3, T4 ' 02 to 05
            If gNote(Note).MeasTime.Pos = _
            gNotePos(pType) Then
                'GoSub UpdateNoteTimes
                'SaveNote = Note ' prev "full chord"
                ' where all four tones of the chord
                ' were input or calculated normally.
                'gNote(Note).xNHT = NHTNONE
                'I = I ' testing
                GoSub MatchedNotes
            Else
                ' CASE Not Matched: possible NHT note
                Do While gNote(Note).MeasTime.Pos _
                < gNotePos(pType) ' eg. E.pos < Q.pos
                    GoSub CheckNHT
                    GoSub CopyDefaultsToTheRest
                    BumpTrace = 4: GoSub BumpNoteNo ' Note=Note+1
                    If bErr Then
                        Return
                    End If
                    ' If Note changes, cannot use
                    '   WITH gNote(Note)
                Loop

                If gNote(Note).MeasTime.Pos > _
                    gNotePos(pType) Then
                    ' CASE Error Should not happen
                    GoSub Debug3
                    GoSub Err9
                    Stop: I = I ' testing
                Else ' CASE Matched note positions
                    'GoSub UpdateNoteTimes
                    'SaveNote = Note ' prev good note
                    GoSub MatchedNotes
                End If
            End If
        Case Else ' invalid type
            Stop: I = I ' testing
    End Select
    Return

'*********************************************

Test1:
    If gSongLast = 1 Then
        If pType - 1 = BASS Then
            I = I ' testing
            If Note - pLast = 6 Then '12jul08
                I = I ' testing
            End If
            If Note - pLast = 5 Then
                I = I ' testing
            End If
        End If
    End If
    Return

'*********************************************
' BEGIN OF GoSub MatchedNotes:
'*********************************************

' CASE Matched: not a NHT note
' The position of this note in the measure
'   gNote(Note).MeasTime.Pos
' matches the position of this note
'   gNotePos(pType)
' from this Field from the Tn: card for
' this Voice = pType-1.
MatchedNotes:
    Select Case UnitsPerWNote
    Case 17
        GoSub UpdateNoteTimesSBar
        'GoSub ResetNotePos
        gNotePos(TM) = 0 ' reset for next measure
    Case 19
        GoSub UpdateNoteTimesDBar
        'GoSub ResetNotePos
        gNotePos(TM) = 0 ' reset for next measure
    Case Is > 0
        GoSub UpdateNoteTimes
        SaveNote = Note ' prev "full chord"
        ' where all four tones of the chord
        ' were input or calculated normally.
        'gNote(Note).xNHT = NHTNONE
        I = I ' testing
    Case Else ' not found in TIMENAME string
        GoSub Err2B
    End Select
    Return

'*********************************************

UpdateNoteTimesSBar:
    'gNote(Note).TMName = TMName$
    'gNote(Note).Time = SINGLEBAR ' -1 "|" bar
    'gNote(Note).bBar = True
    With gNote(Note).Voice(pType - 1).Time
        .Name = UCase$(gField(Field))
        .Val = SINGLEBAR ' -1 "|" bar
        .Units = 0
        .Pos = 0
        gNotePos(pType) = 0
    End With
    gSong(gSongLast).NumSingleBars = _
        gSong(gSongLast).NumSingleBars + 1
    Return

'*********************************************

UpdateNoteTimesDBar:
    'gNote(Note).TMName = TMName$
    'gNote(Note).Time = DOUBLEBAR ' -2 "||" bars
    'gNote(Note).bBar = True
    With gNote(Note).Voice(pType - 1).Time
        .Name = UCase$(gField(Field))
        .Val = DOUBLEBAR ' -2 "||" bars
        .Units = 0
        .Pos = 0
        gNotePos(pType) = 0
    End With
    gSong(gSongLast).NumDoubleBars = _
        gSong(gSongLast).NumDoubleBars + 1
    Return

'*********************************************

    ' Subtract 1 from the pType nos 02-05 to
    ' get Voice numbers 1-4.
UpdateNoteTimes:
    With gNote(Note).Voice(pType - 1).Time
        .Name = UCase$(gField(Field))
        .Val = UnitsPerWNote 'eg:1=W whole note
        TimeUnits = WHOLENOTEUNITS / _
            UnitsPerWNote
        GoSub AdjustTimeUnits
        '.Units = WHOLENOTEUNITS / .Val
        .Units = TimeUnits
        .Pos = gNotePos(pType)
        gNotePos(pType) = .Pos + .Units
        GoSub Debug2
        GoSub InitNote
        GoSub CopyDefaultsToTheRest
    End With
    Return

'*********************************************
' END OF GoSub MatchedNotes:
'*********************************************

'   A harmonic chord is four tones in a vertical
' chord, as displayed in a diatonic music score,
' which make up a valid chord in the music key.
'   For the purposes of this 4Part program,
' which is trying to match
'   Ottman's Elementary Harmony
' book at present, Nov 2004, a valid chord is
' (1) a triad or
' (2) a seventh chord built on the second
' or fifth scale degree,
' that is, ii7 or V7 (IIo7 or v7 in minor).

' The music score will show at every note
' position either:
' (1) "full chord" of four tones in a vertical
'   four part chord; or
' (2) "partial chord" of one to three tones
' where the missing tone(s) are supplied from
' notes prolonged from one or more previous chords.

' If a full chord is followed by a partial chord,
' either or both may be harmonic or non-harmonic.

' The full chord note, SaveNote, is flagged
' as a possible NHT note with bit NHTFIRST
'   gNote(SaveNote).xNHT Or NHTFIRST (Or=Boolean oper)
' The partial chord note, Note, is flagged
' as a possible NHT note with bit NHTHANG
' (meaning HANGing out there by itself)
'   gNote(Note).xNHT Or NHTHANG

' In Sub Harmonize, at a solution or non-solution,
' the GoSubs
'   NHTSolution:
'   FindTheNHTCulPrit:
' will flag the note as NHTHARMONIC or NHTNONHARM
' and will flag the offending tone or voice in the
' chord as .bNHT = True.
'   The Gosub FindTheNHTHANGCulprit: or
' the Gosub FindTheNHTFIRSTCulprit:
' In sub Harmonize
' will add ",N" to the position
' data in grdTable Rows 17-20 for any tone
' flagged as .bNHT = True.
'   The Sub WriteAbc will add a prefix
'   "<(  "">)"^F   if an accidental
'   "<("">)"F      if no accidental.
' to any note tone with ",N" when it writes out
' the ABCPlus music notation data.  Then the
'   abcm2ps
' program will enclose the offending note head
' with parentheses to indicate a NHT tone.

' I assume that nonharmonic notes do not extend
' over rests and bars.

CheckNHT:
    'Return
    ' Flag Note with NHTHANG and
    gNote(Note).xNHT = _
        gNote(Note).xNHT Or NHTHANG 'set

    ' Flag SaveNote with NHTFIRST
    If SaveNote >= Note Then ' insurance
        I = I ' testing (pickup note?)
        Stop: I = I ' testing
    Else ' set the bit
    'ElseIf SaveNote > 0 Then ' set the bit
        gNote(SaveNote).xNHT = _
        gNote(SaveNote).xNHT Or NHTFIRST
    End If

    ' Use a negative
    '   .Time.Offset = SaveNote - Note
    ' value to flag the offset to the
    ' last tone that is prolonged to
    ' form part of this "partial chord".
    With gNote(Note).Voice(pType - 1).Time
            '.Pos = SaveNote - Note ' DO NOT USE
            .Offset = SaveNote - Note
            'GoSub Test4
    End With

    ' TODO This may not be needed.  If needed,
    ' it is used probably only in
    '   Sub Harmonize
    GoSub FindTheNHTCulpritTone
    Return

'*********************************************

Test4:
    With gNote(Note).Voice(pType - 1).Time
      If .Offset = -4 Then
          I = I
      End If
      If .Offset < 0 _
      Or pType - 1 = BASS Then
          GoSub Debug2Force
      End If
    End With
    Return

'*********************************************

' Every time a harmonic tone is used to find
' out that a possible NHT condition exists,
' even though all the T1: to T4: cards have
' not been processed yet, check every voice
' with this Gosub to find the NHT tone(s)
' whose positions in the measure matches
'   gNote(Note).MeasTime.Pos

' If found, flag that voice for that Note with
'   .bNHT
' as the non-harmonic tone (if the chord is
' found by Sub Harmonize to be non harmonic).
FindTheNHTCulpritTone:
    For Voice = SOPRANO To BASS
        With gNote(Note).Voice(Voice)
            If .Time.Pos = gNote(Note).MeasTime.Pos _
            Then
                .bNHT = True
            End If
        End With
    Next Voice
    Return

'*********************************************

'Offset As Integer ' -n means the nth note back
'DefaultPos As Integer ' DefaultPos=.Pos is set
'   by Tn: card for any higher Tn+1: to T4: card,
'   as a convenience in input.

' WARNING:
' Do NOT do this for .Pos due to logic at
' GoSub CheckNHT:

' After getting the values from a Tn: card,
' copy the values to any later Tn: card fields
' as defaults, for convenience in input.
'   E.g. copy the T2: values to T3: and T4:.
' Subtract 1 from the pType nos 02-05 to
' get Voice numbers 1-4.

CopyDefaultsToTheRest:
    'Return ' 1sep08
    If pType - 1 < BASS Then
        If bTMDebug Then
            Debug.Print "StoreTM:D4 ptype="; pType; gTextLine
            Debug.Print "StoreTM:D4 Note; voice; .Name; "; _
                " .Val; .Units; .Offset; .DefaultPos "
        End If
        For Voice = pType To BASS
            With gNote(Note).Voice(Voice).Time
                .Name = gNote(Note).Voice(pType - 1) _
                    .Time.Name
                .Val = gNote(Note).Voice(pType - 1) _
                    .Time.Val
                .Units = gNote(Note).Voice(pType - 1) _
                    .Time.Units
                '.Pos = gNote(Note).Voice(pType - 1) _
                    .Time.Pos  ' DO NOT USE THIS 30apr08 12jul08
                '.Offset = gNote(Note).Voice(pType - 1) _
                    .Time.Offset ' DO NOT USE THIS 12jul08
                .DefaultPos = gNote(Note).Voice(pType - 1) _
                    .Time.Pos
                If bTMDebug Then
                    Debug.Print "StoreTM:D5 "; Note; Voice; _
                        .Name; " "; .Val; .Units; ; .Offset; .DefaultPos
                End If
            End With
        Next Voice
        If bTMDebug Then
            I = I
        End If
    End If
    Return


'*********************************************

Debug2Force:
      bTMDebugSave = bTMDebug
      bTMDebug = True
      GoSub Debug2
      bTMDebug = bTMDebugSave
      Return
      
Debug2:
    If Not bTMDebug Then Return
    'If Field < 20 Or Field > 28 Then Return
    'If Note < 20 Or Note > 42 Then Return
    'If Note > 130 Then
    '    '
    'elseIf Note < 0 Or Note > 42 Then
    '    Return
    'End If
    'If Note < 124 Or Note > 127 Then
    '    Return
    'End If
    With gNote(Note).Voice(pType - 1).Time
        Debug.Print "StoreTM:D2 song,meas="; _
            gSongLast; gNote(Note).MeasureNo; _
            "note,field="; Note; Field; _
            " name, val="; .Name; .Val; _
            " pos, units, offset="; _
            .Pos; .Units; .Offset; _
            "pType-1(voice),gNotePos(pType)="; _
            pType - 1; gNotePos(pType)
    End With
    Return

'*********************************************

Debug3:
    If Not bTMDebug Then Return
    Debug.Print "StoreTM:D12 gNote(Note).MeasTime.Pos="; _
        gNote(Note).MeasTime.Pos; _
        "voice,gNotePos(pType)="; _
        pType - 1; gNotePos(pType)
    Return

'*********************************************
' END EDIT for this T1:-T4: Card
'*********************************************

Err1:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreTM:E1 LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        "reached NOTEMAX="; SONGNOTEMAX; _
        "in "; gTextLine
    Return

'*********************************************

Err2:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreTM:E2 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "invalid TM: field no="; I; _
        " field="; gField(I); _
        " in "; _
        gTextLine
    Debug.Print "StoreTM:E2 LineNo="; _
        gInputLineNo; _
        "TM: field expects: W H Q E S | ||"
    Return

'*********************************************

Err2B:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreTM:E2B LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "invalid TM: field no="; I; _
        " field="; gField(I); _
        " in "; _
        gTextLine
    Debug.Print "StoreTM:E2B LineNo="; _
        gInputLineNo; _
        "TM: field expects: W H Q E S | ||"
    Return

'*********************************************

Err3:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreTM:E3 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "invalid empty time value at TM: field "; _
        Field; "in "; gTextLine
    Debug.Print "StoreTM:E3 LineNo="; _
        gInputLineNo; _
        "TM: field expects: W H Q E S | ||"
    Exit Sub

'*********************************************

Err6:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreTM:E6 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "invalid dotted value "; TMDotted$; _
        " in TM: field no="; I; _
        " field="; gField(I); _
        " in "; _
        gTextLine
    Debug.Print "StoreTM:E6 LineNo="; _
        gInputLineNo; _
        "TM: field expects: W H Q E S | ||"
    Return

'*********************************************

Err7:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreTM:E7 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "invalid n/d value "; TMTime$; _
        " in TM: field no="; I; _
        " field="; gField(I); _
        " in "; _
        gTextLine
    Return

'*********************************************

Err9:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreTM:E9 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "invalid value in TM: fieldno="; Field; _
        " field="; gField(Field); _
        " note="; Note; _
        " measure="; gNote(Note).MeasureNo; _
        " does not match any position "; _
        "from the TM: card in "
    For I = 0 To Len(gTextLine) / 4
        Debug.Print I & Chr$(9);
    Next I
    Debug.Print
    Debug.Print gTextLine

    Debug.Print "StoreTM:E9 LineNo="; _
        gInputLineNo; _
        "TM: field expects: W H Q E S | ||"
    Return

    End Sub

Private Sub StoreTMedit(pbErr As Boolean)
    ' called by sub InputLines

    ' Count the number of single and double
    ' measure bars.
    ' Sub StoreSong checks for at least one
    ' double bar before the next song starts.
    ' TODO
    ' This is probably not needed because of
    ' the following error messages.

    ' First note in the TM: line of the song
    ' cannot be a SINGLEBAR | or a DOUBLEBAR ||.
    ' Last note in the TM: line of the song
    ' must be a DOUBLEBAR ||.

    Dim I As Integer
    Dim FirstNote As Integer
    Dim LastNote As Integer
    Dim nChords
    Dim nDoubleBars

    Dim Note As Integer
    Dim Field As Integer
    Dim K As Integer
    Dim A$
    Dim E$
    Dim TMName$
    Dim Dotted$
    Dim LenDot As Integer
    Const LT As String = "<<<<<"
    Const GT As String = ">>>>>"

'*********************************************
'*********************************************

Main:
    GoSub Init
    GoSub Test1
    If pbErr Then
        Exit Sub
    End If

    GoSub Test2
    If pbErr Then
        Exit Sub
    End If

    GoSub Test3
    Exit Sub

'*********************************************
'*********************************************

Init:
    FirstNote = gSong(gSongLast).FirstNote
    LastNote = gSong(gSongLast).LastNote
    Return

'*********************************************

Test1:
    ' First note of the song cannot be a bar.
    ' Is this because of the flags set for
    ' FirstType chords, etc.?
    ' TODO how do I indicate a repeat bar at the
    ' beginning of the song?
    If gNote(FirstNote).Time = SINGLEBAR Then
        GoSub Err3
    ElseIf gNote(FirstNote).Time = DOUBLEBAR Then
        GoSub Err3
    End If
    Return

'*********************************************

Test2:
    ' last note of the song must be a DOUBLEBAR.
    If gNote(LastNote).Time <> DOUBLEBAR Then
        GoSub Err4
    End If
    Return

'*********************************************

Test3:
    ' The program looks at the TM: line
    ' to check for the following:
    '   Each phrase in a song must be at
    ' least two chord notes followed by
    ' a DOUBLEBAR note (else cannot process
    ' using gNoteX and gAfterX).

    nChords = 0 ' num of chords between DOUBLEBARs
    nDoubleBars = 0
    For I = FirstNote To LastNote ' Clear the array
        With gNote(I)
            If .Time = DOUBLEBAR Then
                nDoubleBars = nDoubleBars + 1
                If nChords < 2 Then
                    GoSub Err5
                Else
                    nChords = 0
                End If
            Else
                nChords = nChords + 1
            End If
        End With
    Next I
    Return

'*********************************************

Err3:
    pbErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreTMedit:E3 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "invalid bar in first TM: field of song. "
    Return

'*********************************************

Err4:
    pbErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreTMedit:E4 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "DOUBLEBAR missing in last TM: field in song."
    I = I
    'stop
    Return

'*********************************************

Err5:
    pbErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreTMedit:E5 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "need at least 2 chord notes before "; _
        "the double bar no.="; nDoubleBars; _
        "at note="; I
    Return

End Sub


Private Sub StoreErrorER(pLast As Integer, _
                    gTextLine As String)
    ' called by sub StoreNotes
    ' Globals set: gNote(Note)...=,gInputErrs =)
    ' Globals used: gFieldLast,gFieldLast, _
        gScale()...,gKeyMode)
    ' Parms set: pLast = Note = .LastUsed(ER)
    '   which is the last/highest note used
    '   in array gNote for this voice.

    ' Format, e.g.:
    '   ER:             BxT
    ' Also need:
    '   OP: ALLOW   PASS1   BXT
    ' but do not need (for PASS4)
    '   OP: ALLOW   BXT

    ' 04may08
    ' The fields on a ER: card are related to the
    ' fields on the TM: card.  Each TAB on an ER:
    ' card moves to the next note on the TM: card.
    ' An ER: card can come anywhere after the TM: card.
    ' More than one ER: card can be used per TM: card.
    ' Trailing blanks or hyphens and bar are optional
    ' after the last valid ER: field.

    ' I may not need this:  04may08
    ' This can be useful when inputting a Bach
    ' chorale where each Vn: has all its notes
    ' on one line, and a key modulation is needed.
    ' If a ER: field is of the format
    '   Ndddd=
    ' where N may be upper or lower case
    ' and ddd is optional number of digits 1-4
    ' then set Note=dddd-1
    ' On the next pass of the loop, the next
    ' ER: field, e.g.
    '   K2: V
    ' will be stored at Note+1 equal to Note+1
    ' on the TM: card.

    Dim I As Integer
    Dim Note As Integer
    Dim K As Integer
    Dim A$
    Dim A2$
    Dim A3$
    Dim B$
    Dim bFound As Boolean
    Dim bRandomERFound As Boolean

'*******************************************
'*******************************************
    
Main:
    GoSub Init
    GoSub MainLine
    Exit Sub

'*******************************************
'*******************************************

Init:
    bFound = False
    bRandomERFound = False
    Return
    
'*******************************************

MainLine:
    Note = pLast
    For I = 1 To gFieldLast
        If Note >= SONGNOTEMAX Then GoTo Err1
        Note = Note + 1
        A$ = UCase$(gField(I))
        ' - is only used for vertical alignment
        ' of the input notes.
        If A$ = "" _
        Or A$ = " " _
        Or A$ = "-" Then
            GoTo NextI
        End If
        If bRandomERFound Then
            bRandomERFound = False
        Else
            GoSub CheckRandomER ' may increase NOTE=
            If bRandomERFound Then
                GoTo NextI
            End If
        End If
        
        If Left$(A$, 3) = "| \" Then ' EOL
            GoTo NextI
        End If
        ' trim "| \" to "|"
        If Right$(A$, 1) = "\" Then
            A$ = Trim$(Left$(A$, Len(A$) - 1))
        End If
        If gNote(Note).ERName = "" Then
            gNote(Note).ERName = A$
        End If
        If Len(A$) = 0 Then
            GoTo NextI
        ElseIf Left$(A$, 1) = "|" Then
            GoTo NextI
        End If
NextI:
    Next I
    'pLast = Note
    Return
    'Exit Sub

'*******************************************

' This can be useful when inputting a Bach
' chorale where each Vn: has all its notes
' on one line, and a key modulation is needed.
' If a ER: field is of the format
'   Ndddd=
' where N may be upper or lower case
' and ddd is optional number of digits 1-4
' then set Note=dddd-1
' On the next pass of the loop, the next
' ER: field, e.g.
'   K2: V
' will be stored at Note+1 equal to Note+1
' on the TM: card.

CheckRandomER:
    bRandomERFound = False ' insurance
    If Right$(A$, 1) = "=" Then ' Random ER: field
        If UCase$(Left$(A$, 1)) = "N" Then
            B$ = Mid$(A$, 2, Len(A$) - 2)
            If IsNumeric(B$) Then
                Note = Val(B$) - 1
                bRandomERFound = True
                Return
            End If
        Else
            GoSub Err4
            Stop
        End If
    End If
    Return

'*******************************************

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreErrorER: LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        "reached NOTEMAX="; SONGNOTEMAX; _
        "in "; gTextLine
    Exit Sub

'*******************************************

Err4:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreErrorER: LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        " invalid format, should be like 'Ndddd='"; _
        " in random ER: subfield no."; _
        I; gField(I); " in "; _
        gTextLine
    Return

End Sub



Private Sub StoreRaiseNoteHS(pLast As Integer, _
                    gTextLine As String)
    ' called by sub StoreNotes
    ' Globals set: gNote(Note)...=,gInputErrs =)
    ' Globals used: gFieldLast,gFieldLast, _
        gScale()...,gKeyMode)
    ' Parms set: pLast = Note = .LastUsed(HS)
    '   which is the last/highest note used
    '   in array gNote for this voice.

    ' Format, e.g.:
    '   HS:             +7

    ' HS cards must come after TM: and before
    ' any Vn: card, so that the RaiseHS value
    ' can be added to the notes.
    
    ' 04may08
    ' The fields on a HS: card are related to the
    ' fields on the TM: card.  Each TAB on an HS:
    ' card moves to the next note on the TM: card.
    ' An HS: card can come anywhere after the TM: card.
    ' More than one HS: card can be used per TM: card.
    ' Trailing blanks or hyphens and bar are optional
    ' after the last valid HS: field.

    ' May not need this random feature:  04may08
    ' This can be useful when inputting a Bach
    ' chorale where each Vn: has all its notes
    ' on one line, and a key modulation is needed.
    ' If a HS: field is of the format
    '   Ndddd=
    ' where N may be upper or lower case
    ' and ddd is optional number of digits 1-4
    ' then set Note=dddd-1
    ' On the next pass of the loop, the next
    ' HS: field, e.g.
    '   HS  -5
    ' will be stored at Note+1 equal to Note+1
    ' on the TM: card.

    Dim I9 As Integer
    Dim Field As Integer
    Dim Note As Integer
    Dim K As Integer
    Dim A$
    Dim A2$
    Dim A3$
    Dim B$
    Dim bFound As Boolean
    Dim bRandomHSFound As Boolean
    Dim HalfStep As Integer
    Dim bNext As Boolean
    
    Const RAISEHSMAX As Integer = 24

'*******************************************
'*******************************************
    
Main:
    GoSub Init
    GoSub MainLine
    pLast = Note
    Exit Sub

'*******************************************
'*******************************************

Init:
    bFound = False
    bRandomHSFound = False
    Return
    
'*******************************************

MainLine:
    Note = pLast
    For Field = 1 To gFieldLast
        If Note >= SONGNOTEMAX Then GoTo Err1
        Note = Note + 1
        A$ = UCase$(gField(Field))
        ' - is only used for vertical alignment
        ' of the input notes.
        GoSub EditFormat
        If bNext Then
            GoTo NextField
        End If
        
        If bRandomHSFound Then
            bRandomHSFound = False
        Else
            'GoSub CheckRandomHS ' may increase NOTE=
            If bRandomHSFound Then
                GoTo NextField
            End If
        End If
        
        GoSub EditNum
        If bNext Then
            GoTo NextField
        End If
        
        gNote(Note).bNewRaiseHS = True
        gNote(Note).RaiseHS = HalfStep
NextField:
    Next Field
    'pLast = Note
    Return

'*******************************************

EditFormat:
      bNext = False
      If A$ = "" _
      Or A$ = " " _
      Or A$ = "-" Then
          GoTo NextI
      End If
      
      If Left$(A$, 3) = "| \" Then ' EOL
          GoTo NextI
      End If
      ' trim "| \" to "|"
      If Right$(A$, 1) = "\" Then
          A$ = Trim$(Left$(A$, Len(A$) - 1))
      End If
      If Len(A$) = 0 Then
          GoTo NextI
      ElseIf Left$(A$, 1) = "|" Then
          GoTo NextI
      End If
      Return
NextI:
      bNext = True
      Return
      
'*******************************************

EditNum:
      bNext = False
      If IsNumeric(A$) Then
          HalfStep = Val(A$)
      Else
          GoSub Err3
          bNext = True
      End If
      If HalfStep > RAISEHSMAX _
      Or HalfStep < -RAISEHSMAX Then
          GoSub Err2
          bNext = True
      End If
      Return
      
'*******************************************

' This random feature
' can be useful when inputting a Bach
' chorale where each Vn: has all its notes
' on one line, and a key modulation is needed.
' If a HS: field is of the format
'   Ndddd=
' where N may be upper or lower case
' and ddd is optional number of digits 1-4
' then set Note=dddd-1
' On the next pass of the loop, the next
' HS: field, e.g.
'   K2: V
' will be stored at Note+1 equal to Note+1
' on the TM: card.

CheckRandomHS:
    bRandomHSFound = False ' insurance
    If Right$(A$, 1) = "=" Then ' Random HS: field
        If UCase$(Left$(A$, 1)) = "N" Then
            B$ = Mid$(A$, 2, Len(A$) - 2)
            If IsNumeric(B$) Then
                Note = Val(B$) - 1
                bRandomHSFound = True
                Return
            End If
        Else
            GoSub Err4
            Stop
        End If
    End If
    Return

'*******************************************

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreRaiseNoteHS: LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        "reached NOTEMAX="; SONGNOTEMAX; _
        "in "; gTextLine
    Exit Sub


'*******************************************

Err2:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreRaiseNoteHS: LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        " out of range= +-"; RAISEHSMAX; _
        "  in field no."; _
        Field; gField(Field); " in "; _
        gTextLine
    Return

'*******************************************

Err3:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreRaiseNoteHS: LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        " not numeric field no."; _
        Field; gField(Field); " in "; _
        gTextLine
    Return

'*******************************************

Err4:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreRaiseNoteHS: LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        " invalid format, should be like 'Ndddd='"; _
        " in random HS: subfield no."; _
        Field; gField(Field); " in "; _
        gTextLine
    Return

End Sub

Private Sub StoreChangeXZRests(pLast As Integer, _
                    gTextLine As String)
    ' called by sub StoreNotes
    ' Globals set: gNote(Note)...=,gInputErrs =)
    ' Globals used: gFieldLast,gFieldLast, _
        gScale()...,gKeyMode)
    ' Parms set: pLast = Note = .LastUsed(XX)
    '   which is the last/highest note used
    '   in array gNote for this voice.

    ' Format, e.g.:
    '   XX:             AT
    '   ZZ:             SB

    ' XX and ZZ cards must come after TM: and before
    ' any Vn: card, so that the S A T or B tone can
    ' be changed by XX to an ABC x (invisible rest) or
    ' be changed by ZZ to an ABC z (rest)
    '
    ' The following comments refer both to XX
    ' and ZZ cards.
    
    ' 04may08
    ' The fields on a XX: card are related to the
    ' fields on the TM: card.  Each TAB on an XX:
    ' card moves to the next note on the TM: card.
    ' An XX: card can come anywhere after the TM: card.
    ' More than one XX: card can be used per TM: card.
    ' Trailing blanks or hyphens and bar are optional
    ' after the last valid XX: field.

    ' May not need this random feature:  04may08
    ' This can be useful when inputting a Bach
    ' chorale where each Vn: has all its notes
    ' on one line, and a key modulation is needed.
    ' If a XX: field is of the format
    '   Ndddd=
    ' where N may be upper or lower case
    ' and ddd is optional number of digits 1-4
    ' then set Note=dddd-1
    ' On the next pass of the loop, the next
    ' XX: field, e.g.
    '   XX  -5
    ' will be stored at Note+1 equal to Note+1
    ' on the TM: card.

    Dim Request$ ' XX or ZZ
    Dim xRequest As Byte ' 128 or 1 or x'00'
    Dim Field As Integer
    Dim Note As Integer
    Dim I As Integer
    Dim A$
    Dim B$
    Dim bNext As Boolean
    Dim bRandomXZFound As Boolean

    Dim I9 As Integer
    Dim K As Integer
    Dim A2$
    Dim A3$
    Dim bFound As Boolean

'*******************************************
'*******************************************
    
Main:
    GoSub Init
    GoSub MainLine
    pLast = Note
    Exit Sub

'*******************************************
'*******************************************

Init:
    bFound = False
    bRandomXZFound = False
    Request$ = UCase$(Left$(gTextLine, 2)) ' XX or ZZ
    If Request$ = "XX" Then
        xRequest = WRITEX
    ElseIf Request$ = "ZZ" Then
        xRequest = WRITEZ
    Else
        Stop
    End If
    Return
    
'*******************************************

MainLine:
    Note = pLast
    For Field = 1 To gFieldLast
        If Note < SONGNOTEMAX Then
            Note = Note + 1
        Else
            GoSub Err1
            GoTo MainLineX
        End If
        A$ = UCase$(gField(Field))
        ' - is only used for vertical alignment
        ' of the input notes.
        GoSub EditFormat
        If bNext Then
            GoTo NextField
        End If
        
        If bRandomXZFound Then
            bRandomXZFound = False
        Else
            'GoSub CheckRandomXZ ' may increase NOTE=
            If bRandomXZFound Then
                GoTo NextField
            End If
        End If
        
        GoSub EditVoiceNames
NextField:
    Next Field
    'pLast = Note
MainLineX:
    Return

'*******************************************

EditFormat:
      bNext = False
      If A$ = "" _
      Or A$ = " " _
      Or A$ = "-" Then
          GoTo NextI
      End If
      
      If Left$(A$, 3) = "| \" Then ' EOL
          GoTo NextI
      End If
      ' trim "| \" to "|"
      If Right$(A$, 1) = "\" Then
          A$ = Trim$(Left$(A$, Len(A$) - 1))
      End If
      If Len(A$) = 0 Then
          GoTo NextI
      ElseIf Left$(A$, 1) = "|" Then
          GoTo NextI
      End If
      Return
NextI:
      bNext = True
      Return
      
'*******************************************

EditVoiceNames:
    For I = 1 To Len(A$)
        With gNote(Note)
            B$ = UCase$(Mid$(A$, I, 1))
            Select Case B$
                Case "S"
                    .Voice(SOPRANO).xWriteXZ = _
                    .Voice(SOPRANO).xWriteXZ Or xRequest
                Case "A"
                    .Voice(ALTO).xWriteXZ = _
                    .Voice(ALTO).xWriteXZ Or xRequest
                Case "T"
                    .Voice(TENOR).xWriteXZ = _
                    .Voice(TENOR).xWriteXZ Or xRequest
                Case "B"
                    .Voice(BASS).xWriteXZ = _
                    .Voice(BASS).xWriteXZ Or xRequest
                Case Else
                    GoSub Err3
            End Select
        End With
    Next I
    Return
      
'*******************************************

' This random feature
' can be useful when inputting a Bach
' chorale where each Vn: has all its notes
' on one line, and a key modulation is needed.
' If a XX: field is of the format
'   Ndddd=
' where N may be upper or lower case
' and ddd is optional number of digits 1-4
' then set Note=dddd-1
' On the next pass of the loop, the next
' XX: field, e.g.
'   K2: V
' will be stored at Note+1 equal to Note+1
' on the TM: card.

CheckRandomXZ:
    bRandomXZFound = False ' insurance
    If Right$(A$, 1) = "=" Then ' Random XX: field
        If UCase$(Left$(A$, 1)) = "N" Then
            B$ = Mid$(A$, 2, Len(A$) - 2)
            If IsNumeric(B$) Then
                Note = Val(B$) - 1
                bRandomXZFound = True
                Return
            End If
        Else
            GoSub Err4
            Stop
        End If
    End If
    Return

'*******************************************

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreChangeXZRests: LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        "reached NOTEMAX="; SONGNOTEMAX; _
        "in "; gTextLine
    Return

'*******************************************

Err3:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreChangeXZRests: LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        " voice letter(s) must be S A T or B"; _
        "  in field no."; _
        Field; gField(Field); " in "; _
        gTextLine
    Return

'*******************************************

Err4:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreChangeXZRests: LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        " invalid format, should be like 'Ndddd='"; _
        " in random XX: subfield no."; _
        Field; gField(Field); " in "; _
        gTextLine
    Return

End Sub





Private Sub StoreRN(pLast As Integer, _
                    gTextLine As String)
    ' called by sub StoreNotes
    ' Globals set: gNote(Note)...=,gInputErrs =)
    ' Globals used: gFieldLast,gFieldLast, _
        gScale()...,gKeyMode)
    ' Parms set: pLast = Note = .LastRN
    '   which is the last/highest note used
    '   in array gNote for this voice.

    ' 12/11/6.
    ' The fields on a RN: card are related to the
    ' fields on the TM: card.  Each TAB on an RN:
    ' card moves to the next note on the TM: card.
    ' An RN: card can come anywhere after the TM: card.
    ' More than one RN: card can be used per TM: card.
    ' Trailing blanks or hyphens and bar are optional
    ' after the last valid RN: field.

    ' If a new modulation key is needed,
    '   K2:, K3:, or K4:,
    ' the RN: card should come before
    ' the Vn: cards if that note contains
    ' a chord in the new key.
    ' (Sometimes the note contains a chord
    ' that works in both the old and the new
    ' key where no new accidental is used.)

    ' If you use an RN: card with a Kn: field
    ' before the Vn: cards, then the RN: card
    ' can contain normal RomNum fields too, or
    ' you can have another RN: card following
    ' the Vn: cards.  The second RN: normally
    ' should not also have a Kn: field; if it does,
    ' it overrides the Kn: field of the previous
    ' RN: card. The field
    '   gNote(Note).RNName
    ' will concatenate the fields from both
    ' RN: cards.

    ' This can be useful when inputting a Bach
    ' chorale where each Vn: has all its notes
    ' on one line, and a key modulation is needed.
    ' If a RN: field is of the format
    '   Ndddd=
    ' where N may be upper or lower case
    ' and ddd is optional number of digits 1-4
    ' then set Note=dddd-1
    ' On the next pass of the loop, the next
    ' RN: field, e.g.
    '   K2: V
    ' will be stored at Note+1 equal to Note+1
    ' on the TM: card.


    Dim I As Integer
    Dim Note As Integer
    Dim K As Integer
    Dim A$
    Dim A2$
    Dim A3$
    Dim B$
    Dim bFound As Boolean
    Dim bRandomRNFound As Boolean

'*******************************************
'*******************************************
    
Main:
    GoSub Init
    GoSub MainLine
    Exit Sub

'*******************************************
'*******************************************

Init:
    bFound = False
    bRandomRNFound = False
    Return
    
'*******************************************

MainLine:
    Note = pLast
    For I = 1 To gFieldLast
        If Note >= SONGNOTEMAX Then GoTo Err1
        Note = Note + 1
        A$ = gField(I)
        ' - is only used for vertical alignment
        ' of the input notes.
        If A$ = "-" Then
            GoTo NextI
        End If
        If bRandomRNFound Then
            bRandomRNFound = False
        Else
            GoSub CheckRandomRN
            If bRandomRNFound Then
                GoTo NextI
            End If
        End If
        
        If Left$(A$, 3) = "| \" Then ' EOL
            GoTo NextI
        End If
        ' trim "| \" to "|"
        If Right$(A$, 1) = "\" Then
            A$ = Trim$(Left$(A$, Len(A$) - 1))
        End If
        If gNote(Note).RNName = "" Then
            gNote(Note).RNName = A$
        Else ' concatenate the Kn: with a later RN: field
            If Left$(gNote(Note).RNName, 1) = "K" Then
                gNote(Note).RNName = _
                    gNote(Note).RNName & A$
            Else
                gNote(Note).RNName = A$
            End If
        End If
        If Len(A$) = 0 Then
            GoTo NextI
        ElseIf Left$(A$, 1) = "|" Then
            GoTo NextI
        ElseIf Left$(A$, 1) = "K" Then
            GoSub Modulation
            If Len(A$) = 0 Then ' may be blanked out
                GoTo NextI ' 16jul08
            End If
        End If
        ' Check if the input roman numeral, e.g. V,
        ' is found in the current scale and scale mode.
        For K = TONIC To LEADINGTONE ' 1 to 7
            If A$ = _
            gScale(gKeyMode).RNumName(K) Then
                bFound = True
                gNote(Note).RomNum = K ' scale deg
                ' of the root
                gNote(Note).Quality = _
                    gScale(gKeyMode).Qual(K)
                '   1=maj,2=min,3=dim,4=aug
            End If
        Next K
        If Not bFound Then
            GoTo Err2
        End If
NextI:
    Next I
    'pLast = Note
    Return
    'Exit Sub

'*******************************************

' This can be useful when inputting a Bach
' chorale where each Vn: has all its notes
' on one line, and a key modulation is needed.
' If a RN: field is of the format
'   Ndddd=
' where N may be upper or lower case
' and ddd is optional number of digits 1-4
' then set Note=dddd-1
' On the next pass of the loop, the next
' RN: field, e.g.
'   K2: V
' will be stored at Note=dddd equal to Note=dddd
' on the TM: card.

CheckRandomRN:
    bRandomRNFound = False ' insurance
    If Right$(A$, 1) = "=" Then ' Random RN: field
        If UCase$(Left$(A$, 1)) = "N" Then
            B$ = Mid$(A$, 2, Len(A$) - 2)
            If IsNumeric(B$) Then
                Note = Val(B$) - 1
                bRandomRNFound = True
                Return
            End If
        Else
            GoSub Err4
            Stop
        End If
    End If
    Return

'*******************************************

' At any note in a song, you can change to a
' new modulation key by entering an index to
' a key that has already been entered, K2 to K4,
' before the Roman numeral, e.g. K2:V
' Revert back to the original key with K1:
Modulation:
    B$ = "" ' 16jul08
    If Len(A$) > 3 Then
        B$ = Mid$(A$, 4)
        A$ = Left$(A$, 3)
    ElseIf Len(A$) < 3 Then
        GoTo Err3
    End If
    A2$ = Mid$(A$, 2, 1)
    A3$ = Mid$(A$, 3, 1)
    If A2$ < "1" Then
        GoTo Err3
    ElseIf A2$ > SONGKEYMAX Then ' 4
        GoTo Err3
    ElseIf A3$ <> ":" Then
        GoTo Err3
    End If
    gSongKey = Val(A2$)
    gNote(Note).SongKey = gSongKey
    ' At the end of a song or at the end
    ' of all input, Sub FillWithKeys will
    ' propagate a non-zero modulation key
    '   gNote(Note).SongKey
    ' to all notes to the right until the
    ' next modulation key is found.
    
    A$ = B$ ' any following rom num, eg V in K2:V
    Return

'*******************************************

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreRN:E1 LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        "reached NOTEMAX="; SONGNOTEMAX; _
        "in "; gTextLine
    Exit Sub

'*******************************************

Err2:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreRN:E2 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "invalid RN: field no."; _
        I; gField(I); " in "; _
        gTextLine
    'Stop
    Exit Sub
    'Print gNote(Note).RomNum;

'*******************************************

Err3:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreRN:E3 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "invalid Key subfield 'Kn:' in RN: field no."; _
        I; gField(I); " in "; _
        gTextLine
    'Stop
    Exit Sub

'*******************************************

Err4:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreRN:E4 LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        " invalid format, should be like 'Ndddd='"; _
        " in random RN: subfield no."; _
        I; gField(I); " in "; _
        gTextLine
    Return

End Sub





Private Sub StoreFB(pLast As Integer, _
                    gTextLine As String)
    ' called by sub StoreNotes
    ' calls sub Parse

    ' pLast = .LastUsed(FB)
    ' which is the last/highest note used
    ' in array gNote for this voice.
    
    ' 12/11/6.
    ' The fields on a FB: card are related to the
    ' fields on the TM: card.  Each TAB on an FB:
    ' card moves to the next note on the TM: card.
    ' An FB: card can come anywhere after the TM: card.
    ' More than one FB: card can be used per TM: card.
    ' Trailing blanks or hyphens and bar are optional
    ' after the last valid FB: field.
    
    ' On input, a field on the FB: line,
    ' figured bass, may have up to three
    ' single digits, each separated by a single
    ' space, in the inversion field, e.g. 5 3.
    ' The inversion field may be followed by a
    ' comma or single blank and an alter field, e.g. #3.
    ' A leading # or /, e.g. #3 or #6 or /6,
    ' in any field means to raise by a half step
    ' any note which is a third or sixth "above
    ' the bass note" in any octave.
    ' A leading b means to lower it a half step.

    ' A leading #^ or /^, e.g. #^6 or /^6, in any field
    ' means to raise by a half step any note which
    ' is at that "scale degree" in any octave.
    ' A leading b^ means to lower it a half step.
    ' E.g. in Dies Irae in key of D Dorian, to
    ' lower the B to Bb, the sixth scale degree,
    ' enter the note B as B,
    ' and enter b^6 in the FB field under it.

    ' If you check the cp/fb table just below,
    ' you will see that you can not tell the
    ' desired inversion from #3 or #6 or /6
    ' since they occur in several inversions.
    ' (In my program I will treat #3 as a root
    ' pos indicator. - no longer as of 10/28/06)
    ' You can specify an inversion with the
    ' alter symbols: b or # or /.
    ' To get the augmented 5th #^5
    ' in first inversion, must use "6 #3" in
    ' my program.

    ' The program will accept one or more digits
    ' separated by blanks for each of the three
    ' FB subfields.
    
    ' NOTE - 10/23/06
    ' Each column indicates an inversion.
    ' The chord positions above the bass may be
    ' in any order but this is not a problem.
    ' E.g. #3 means to raise the
    ' note which is at the interval of a third
    ' above the bass.  It doesn't matter if the
    ' interval is actually a third plus an octave
    ' above the bass (3th + 8th = 10th).
    
    ' cp = triad chord pos; fb figured bass

    ' triad:
    '                  3  6
    '          1  6    1  4
    '  5  5    5 (3)   5 (1)
    '  3  3    3 (1)
    '  1 (1)
    ' cp fb   cp fb   cp fb

    ' seventh chord:
    '                           5 (6)
    '                   3 (6)   3  4
    '           1  6    1  4    1  2
    '  7  7     7  5    7  3    7 (1)
    '  5 (5)    5 (3)   5 (1)  [5 (6)]
    '  3 (3)    3 (1)  [3 (6)] [3  4]
    '  1 (1)   [1  6]  [1  4]  [1  2]
    ' cp fb    cp fb   cp fb   cp fb
    'rootpos  1stinv  2ndinv  3rdinv

    ' TODO
    ' I J K should be local variables, reusable
    ' freely in any (small) gosub.
    
    Static LastUsedSave As Integer
    
    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim A$
    Dim B$
    Dim C$
    Dim Field$
    Dim InvField$
    Dim AltField$
    Dim Field1 As Integer
    Dim Note1 As Integer
    Dim Sub1 As Integer
    Dim Interval As Integer
    Dim Error As Integer

    Const BLANKVAL As Integer = 32
    Dim SubFieldLast As Integer ' highest used index
    Dim SubField(FBFIELDMAX) As String ' 4
  
'**********************************************
'**********************************************

Main:
    GoSub Init1
    GoSub MainLine
        'GoSub Init
        'GoSub ParseField
            'GoSub GetSubFields
                'call Parse(A$,
            'GoSub ConcatInversionField
            'GoSub GetInversion
            'GoSub ParseAlter
                'GoSub GetInterval
                'GoSub SetAboveBass
                'GoSub SetSDeg
    Exit Sub

'**********************************************
'**********************************************
    
Init1:
    ' TODO
    'If gSongLast = 6 Then ' song 23 is Assn55(6)
    '    J = J
    'End If
    'If gSongLast = 1 Then
    '    J = J ' debug
    'End If
    Note1 = pLast ' pLast = .LastUsed(FB)
    If Note1 >= SONGNOTEMAX Then
        GoTo Err1
    Else
        Note1 = Note1 + 1
        ' Note1=pLast=.LastUsed(FB) is still the same
        ' value that was init in StoreSong. FB = 11.
        ' TODO probably not needed
        ' TODO 12/11/6
        'If gSong(gSongLast).LastUsed(FB) >= _
        '    gSong(gSongLast).FirstNote - 1 Then
        '    GoSub Init
        'Else ' TODO ?
        '    J = J
        '    Stop
        GoSub Init
    End If
    Return
    
'**********************************************

    ' Each new TM: card resets all the
    '   gsong(gsonglast).lastused(cardtype) =
    '   gsong(gsonglast).lastused(TM)
    ' To allow more than one FB: card per
    ' TM: card, only GoSub Init once per
    ' TM: card.
Init:
    gNote(Note1).bSeventh = False ' if early exit
    SubFieldLast = 0
    ' TODO not needed
    'gNote(Note1).NumFB = SubFieldLast
    'For Sub1 = 1 To FBFIELDMAX ' Clear the array
    '    gNote(Note1).FigBass(Sub1) = 0
    'Next Sub1

    ' TODO 12/11/6
    If Not gNote(Note1).bAlterAboveBass Then
        For Sub1 = 1 To ALTERMAX ' Clear the array
            gNote(Note1).AlterAboveBass(Sub1) = 0
        Next Sub1
        gNote(Note1).bAlterAboveBass = False
        gNote(Note1).AlterAboveBassNM = ""
    End If

    If Not gNote(Note1).bAlterSDeg Then
        For Sub1 = TONIC To LEADINGTONE ' Clear array
            gNote(Note1).AlterSDeg(Sub1) = 0
        Next Sub1
        gNote(Note1).bAlterSDeg = False
        gNote(Note1).AlterSDegNM = ""
    End If
    Return

'**********************************************
    
MainLine:
    For Field1 = 1 To gFieldLast
        ' TODO not needed
        'If Note1 >= SONGNOTEMAX Then GoTo Err1
        'Note1 = Note1 + 1
        A$ = Trim$(gField(Field1)) ' e.g. A$="6 #3"
        ' - is only used for vertical alignment
        ' of the input notes. 25/03/08
        If A$ = "-" Then
            'GoTo NextI2
        End If
        
        ' replace any comma with a blank, as insurance.
        A$ = fcnRepChar(A$, ",", " ")
        Select Case A$
            Case "|", "||"
                GoTo NextI
            Case "", "-"
                gNote(Note1).Inversion = NOINVERSIONNEEDED
                ' no entry for this optional field
                GoTo NextI
        End Select
        gNote(Note1).FBName = A$ ' for ease of display
        GoSub ParseField
NextI:
        If Note1 >= SONGNOTEMAX Then
            GoTo Err1
        Else ' match the Vn notes and the FB fields
            Note1 = Note1 + 1
            GoSub Init
        End If
NextI2:
    Next Field1
    ' TODO 12/11/6
    'pLast = Note1 - 1
    Return

'**********************************************

ParseField:
        GoSub GetSubFields
        Field$ = ""
        GoSub ConcatInversionField
        If InvField$ <> "" Then
            GoSub GetInversion
        End If
        
        'gNote(Note1).AlterAboveBassNM = ""
        'gNote(Note1).AlterSDegNM = ""
        ' TODO not needed
        'For Sub1 = 1 To SubFieldLast
        If AltField$ <> "" Then
            If Mid$(AltField$, 2, 1) = "^" Then
                B$ = Left$(AltField$, 2)
            Else
                B$ = Left$(AltField$, 1)
            End If
            GoSub ParseAlter
        End If
        'Next Sub1
        'gNote(Note1).NumFB = SubFieldLast

        'If Field$ = "" Then
        '    Field$ = Trim$(gField(Field1))
        'Else
        '    Field$ = LTrim$(Field$) ' =gField(Field1)
        '    ' without the b,n,# chars
        'End If
    Return
        
'**********************************************

' Parse A$ into non-blank subfields, using blank(s)
' as separators.
GetSubFields:
        ' TODO
        'If Len(A$) <= 2 Then ' only one subfield
        '    SubFieldLast = 1
        '    SubField(SubFieldLast) = A$
        'ElseIf Len(A$) = 3 _
        'And InStr(A$, "^") > 0 Then
        
        If InStr(A$, " ") = 0 Then
            ' only one subfield
            SubFieldLast = 1
            SubField(SubFieldLast) = A$
        Else
            ' parse the text string a$ into
            ' subfield's, using BLANK separator
            SubFieldLast = 0
            Call Parse(A$, SubField(), _
                FBFIELDMAX, SubFieldLast, _
                BLANKVAL)
        End If
        Return
'MyVar = "53"    ' Assign value.
'MyCheck = IsNumeric(MyVar)  ' Returns True.

'**********************************************

' May have up to three inversion digits, e.g. 6 5
' which may be followed by a single alter field
' beginning with b or # or /
' E.g. 6 5 #3
ConcatInversionField:
    InvField$ = ""
    AltField$ = ""
    For Sub1 = 1 To SubFieldLast
        If IsNumeric(SubField(Sub1)) Then
            InvField$ = InvField$ & " " & SubField(Sub1)
        Else
            AltField$ = SubField(Sub1)
            Exit For
        End If
    Next Sub1
    InvField$ = Trim$(InvField$)
    Return

'**********************************************

' TODO See array in fcnGetFBName
GetInversion:
    Select Case InvField$
        Case "5 3"  ' "1", "5", "3",
            gNote(Note1).bSeventh = False
            gNote(Note1).Inversion = ROOTPOS
        Case "6"  ' "6 3"
            gNote(Note1).bSeventh = False
            gNote(Note1).Inversion = FIRSTINV
        Case "6 4"  ' "4"
            gNote(Note1).bSeventh = False
            gNote(Note1).Inversion = SECONDINV
        Case "7"  ' "7 5", "7 3", "7 5 3"
            gNote(Note1).bSeventh = True
            gNote(Note1).Inversion = ROOTPOS
        Case "6 5"  ' "6 5 3"
            gNote(Note1).bSeventh = True
            gNote(Note1).Inversion = FIRSTINV
        Case "4 3"  ' "6 4 3"
            gNote(Note1).bSeventh = True
            gNote(Note1).Inversion = SECONDINV
        Case "4 2"  ' "6 4 2"
            gNote(Note1).bSeventh = True
            gNote(Note1).Inversion = THIRDINV
        ' This 7TH causes: reached CANDMAX
        Case "7TH"
            gNote(Note1).bSeventh = True
        Case Else
            GoTo Err2
    End Select
    Return

'**********************************************

ParseAlter:
    With gNote(Note1)
        Select Case B$
            Case "b"
                GoSub GetInterval
                ' lower this note above the
                ' bass by one half step
                .AlterAboveBass(Interval) _
                    = INCRFLAT ' -1
                GoSub SetAboveBass
            'Case "#", "/", "x", "n", "="
            Case "#", "/", "x", "="
                GoSub GetInterval
                ' Raise this note above the
                ' bass by one half step.
                ' E.g. in C minor, the natural sign
                ' n or = in n3 or =3 is used
                ' to raise Bb in v:G Bb D to B.
                ' 'x' is used to raise the natural
                ' notename by one whole step, e.g.
                ' xF3 is the same note as G3.
                ' In my program FB: x3 is treated
                ' the same as FB: #3 because both
                ' raise a note in the key signature
                ' by a half step.  So x3 is useful
                ' for human eyes.
                .AlterAboveBass(Interval) _
                    = INCRSHARP ' +1
                GoSub SetAboveBass
            Case "b^"
                GoSub GetInterval
                ' lower this scale degree note
                ' by one half step
                .AlterSDeg(Interval) _
                    = INCRFLAT ' -1
                GoSub SetSDeg
            Case "#^", "/^"
                GoSub GetInterval
                ' raise this scale degree note
                ' by one half step
                .AlterSDeg(Interval) _
                    = INCRSHARP ' +1
                GoSub SetSDeg
            Case Else
                If IsNumeric(B$) Then
                    If Field$ = "" Then
                        Field$ = B$
                    Else
                        Field$ = Field$ & " " _
                            & B$
                    End If
                Else
                    Error = 2: GoTo Err4
                End If
        End Select
    End With
    Return

'**********************************************

GetInterval:
    C$ = Mid$(AltField$, Len(B$) + 1)
    If IsNumeric(C$) Then
        Field$ = Trim$(Field$ & " " & C$)
        Interval = Val(C$)
        If Len(B$) = 1 Then ' e.g. #3
            If Interval < 1 Or Interval > ALTERMAX Then
                Error = 3: GoTo Err4
            End If
        ElseIf Len(B$) = 2 Then ' e.g. #^3
            If Interval < TONIC _
            Or Interval > LEADINGTONE Then
                Error = 3: GoTo Err4
            End If
        End If
        ' TODO not needed
        ' Store in subfield(Sub1) of FB the number above
        ' the bass that is to be raised or lowered.
        'gNote(Note1).FigBass(Sub1) = Interval
    Else
        Error = 4: GoTo Err4
        ' TODO better:
        Interval = 0
        Error = 4: GoTo Err4
    End If
    Return

'**********************************************

SetAboveBass:
    With gNote(Note1)
        .bAlterAboveBass = True
        .AlterAboveBassNM = AltField$
        ' TODO not needed
        'If .AlterAboveBassNM = "" Then
        '    .AlterAboveBassNM = SubField(Sub1)
        'Else
        '    .AlterAboveBassNM = _
        '        .AlterAboveBassNM & "," & _
        '        SubField(Sub1)
        'End If
    End With
    Return

'**********************************************

SetSDeg:
    With gNote(Note1)
        .bAlterSDeg = True
        .AlterSDegNM = AltField$
        ' TODO not needed
        'If .AlterSDegNM = "" Then
        '    .AlterSDegNM = SubField(Sub1)
        'Else
        '    .AlterSDegNM = _
        '        .AlterSDegNM & "," & _
        '        SubField(Sub1)
        'End If
    End With
    Return

'**********************************************

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreFB: LineNo="; _
        gInputLineNo; _
        "song no="; gSongLast; _
        "reached NOTEMAX="; SONGNOTEMAX; _
        "in "; gTextLine
    'Exit Sub
    Stop

'**********************************************

Err2:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreFB: LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "more than three digits in field="; _
        gField(Field1); " in "; _
        gTextLine
    'Exit Sub
    Stop

'**********************************************

Err3:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreFB: LineNo="; _
        gInputLineNo; _
        "song="; gSongLast; _
        "invalid FB: fieldno="; Field1; "field="; _
        gField(Field1); " in "; gTextLine
    'Exit Sub
    Stop

'**********************************************

Err4:
    gInputErrs = gInputErrs + 1
    Debug.Print "StoreFB: LineNo="; _
        gInputLineNo; " Error="; Error; _
        "song="; gSongLast; _
        "invalid FB: fieldno="; Field1; "field="; _
        gField(Field1); _
        " subfieldno="; Sub1; "subfield="; _
        SubField(Sub1); " in "; gTextLine
    'Exit Sub
    Stop
    'Print gNote(Note1).FigBass(Sub1);
End Sub


Private Function fcnGetFBName( _
                    pInversion As Integer, _
                    pbSeventh As Boolean) _
                    As String
    ' called by sub EditChord
    ' calls

    ' FB: figured bass may have up to three
    ' single digits, each separated by a single
    ' space.  A leading +, e.g. +3,
    ' means to raise the note a half step.
    ' +3 is stored as a negative 3, used
    ' as an internal flag.

    ' TODO
    ' When using this fcnGetFBName to convert
    ' gInversion and gbSeventh to text figure bass
    ' try to display +3 in a minor key scaledeg 5,
    ' or +6 or +7 in minor melodic key if the
    ' next note is ascending.

    ' TODO See array in StoreFB
    Dim FigBass As Variant
    FigBass = Array("5 3", "6", "6 4", _
                    "7", "6 5", "4 3", "4 2")
    If pbSeventh Then
        If pInversion >= 0 And pInversion <= 3 Then
            fcnGetFBName = FigBass(pInversion + 3)
        Else
            GoTo Err1
        End If
    Else
        If pInversion >= 0 And pInversion <= 2 Then
            fcnGetFBName = FigBass(pInversion)
        Else
            GoTo Err1
        End If
    End If
    Exit Function

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "fcnGetFBName: invalid "; _
        "pInversion="; pInversion; _
        " or pbSeventh="; pbSeventh; _
        " to get figure bass in song no="; _
        gSongLast
    Exit Function
End Function


Private Function fcnPack(pString As String) _
                As String
    ' called from sub StoreVoiceVn
    ' called from subWriteAbcNotes
    
    ' Remove all blanks from the string and
    ' left-adjust all chars.
    Dim A$
    Dim C$
    Dim I As Integer
    A$ = ""
    For I = 1 To Len(pString)
        C$ = Mid$(pString, I, 1)
        If C$ <> " " Then
            A$ = A$ & C$
        End If
    Next I
    fcnPack = A$
End Function

Private Sub Harmonize(pFirstNote As Integer, _
                    pLastNote As Integer, _
                    pAfterX As Integer, _
                    pErr As String)
    ' called by sub cmdHarmony_Click
    ' called by sub cmdNextSolution_Click

    ' calls sub cmdChords_Click
    ' calls sub DisplayCandChords
    ' calls sub DisplayText
    ' calls sub FinalTypeChord
    ' calls sub FirstTypeChord
    ' callS SUB PrevTypeChord
    ' calls sub DebugNote
    ' calls sub DisplayHarm
    ' calls function fcnbNextBest
    ' calls function fcnNoteIndex

    ' Globals used: gSong()..., gNote(gNoteX)...
    ' Globals set: gNote(I).BestCand = 0, gNoteX =

    ' Note - pass pLastNOte as J = gnotex - 1.
    '       If you pass gNoteX as pLastNote, then
    '       any change to gNoteX in this proc
    '       will also change pLastNote.

    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim BackNote As Integer
    Dim bFoundMatch As Boolean
    Dim E$

    Dim bBackTracking As Boolean
    Dim ErrorAtgNoteX As Integer
    Dim PassNum As Integer ' 1 to 4
    Dim Bc As Integer
    Dim bErr As Boolean
    'Dim bNHT As Boolean
    Dim Note As Integer
    Dim Voice As Integer
    Dim V As Integer ' Voice
    Dim V2 As Integer  ' Voice
    Dim bNHTError As Integer
    Dim NewgAfterX As Integer
    Dim HangIx As Integer
    Dim FirstIx As Integer
    Dim HangName As String
    Dim FirstName As String
    Dim bFirstNameFound As Boolean
    Dim bHangNameFound As Boolean
    Dim DebugSong As Integer
    Dim DebugNote1 As Integer
    Dim DebugNote2 As Integer
    Dim nUserInput As Integer
    Dim nTonesInput As Integer
    Dim SaveGNoteX As Integer
    Dim SaveGAfterX As Integer
    Dim NoteAfter As Integer
    Dim Note2 As Integer
    Dim nNegOffsets As Integer
    Dim SavepFirstNote As Integer

'***********************************************
'***********************************************

Main:
    GoSub Init
    GoSub MainLoop
        '(Call DebugNote)
        'GoSub CheckNewChords
            'call cmdChords_Click
            'GoSub Debug1
        'GoSub ReuseCandSlots
        'GoSub HarmonizeThisNote
            'call FinalTypeChord
            'GoSub Err6
            'call FirstTypeChord
            'call PrevTypeChord
            'GoSub SolutionFound
                '(Call DebugNote)
            'GoSub NoSolutionFound
                '(Call DebugNote)
                'Stop (> 100000)
                'GoSub Goback
                    'GoSub NextBest
                'call DebugNote
                'call DisplayCandChords
                'call DisplayHarm
                'GoSub DisplayGoBacks
                'Stop (after pass 4)
    GoSub Conclusion
        'call DisplayCandChords
        'GoSub DisplayGoBacks
    Exit Sub

'***********************************************
'***********************************************

Init:
    PassNum = 1
    gbErrorFourthPass = False ' see fcnErrAllow
    gNumGoBacks = 0 ' 18aug08
    
    ' 12/12/6 Testing Ottman Fig 11.3 pg 237
    ' At label Test2:
    'DebugSong = 0 ' 0=no debug
    'DebugSong = 1 ' 27sep08
    DebugNote1 = 2 ' set to notes to display
    DebugNote2 = 2
    
    Return

'***********************************************

MainLoop:
    gAfterX = pAfterX ' =0 the first time
    gNoteX = pLastNote
    'For gNoteX = pLastNote To pFirstNote Step -1
    SavepFirstNote = pFirstNote
    'Do While gNoteX >= pFirstNote
    Do While gNoteX >= SavepFirstNote
    With gNote(gNoteX)
        '.Inversion = NOINVERSIONNEEDED ' -1
        gBeforeX = 0
        If .bBar Then ' skip single or double bar
            GoTo MainLoopX
        End If
        If .bRestAll4 Then ' all 4 voices rest
            GoTo MainLoopX
        End If
        ' try to create chords only for note on the beat
        If .Beat = 0 Then ' 21mar08
            'GoTo MainLoopX
        End If
        GoSub Test3
        If gNote(gNoteX).xNHT And NHTHANG Then
            GoSub CheckNHTDelayed ' 28sep08
            If gNote(gNoteX).bNHTDelayed Then
                GoTo MainLoopX
            End If
        End If
        GoSub CheckNewChords
        'GoSub ReuseCandSlots
        GoSub HarmonizeThisNote
        If gNote(gNoteX).BestCand > 0 Then
            GoSub ProcessNHTDelayed ' 28sep08
        End If
    End With ' Must END WITH before changing gNoteX
MainLoopX:
    gNoteX = gNoteX - 1
    Loop
    Return
    
'***********************************************

Test3:
    'If gNoteX = 26 Or gNoteX = 27 Or gNoteX = 28 Then
    If gNoteX = 2 Then
        I = I
    End If
    Return
    
'***********************************************

    ' Count the number of voices with an input note
CheckNHTDelayed:
    nUserInput = 0
    For Voice = SOPRANO To BASS
        K = fcnNoteIndex(gNoteX, Voice, bErr) ' call
        If K <> 0 Then
            nUserInput = nUserInput + 1
        End If
    Next Voice
    
    GoSub CountNegOffsets
    If nUserInput >= nNegOffsets Then
        gNote(gNoteX).bNHTDelayed = False
    Else
        gNote(gNoteX).bNHTDelayed = True
    End If
    Return
    
'***********************************************
        
CountNegOffsets:
    nNegOffsets = 0
    For Voice = SOPRANO To BASS
        If gNote(gNoteX).Voice(Voice).Time.Offset < 0 Then
            nNegOffsets = nNegOffsets + 1
        End If
    Next Voice
    Return
    
'***********************************************

ProcessNHTDelayed:
    SaveGNoteX = gNoteX
    SaveGAfterX = gAfterX
    Note2 = fcnNoteAfter(gNoteX) ' gnotex + 1 30sep08
    If Note2 > 0 Then
        GoSub GetGAfterX
        For gNoteX = Note2 To gAfterX
            If gNote(gNoteX).bNHTDelayed Then
                'GoSub Test3
                GoSub HarmonizeThisNote
                If gNote(gNoteX).BestCand > 0 Then
                    I = I
                Else
                    I = I
                End If
            End If
        Next gNoteX
    End If
    gNoteX = SaveGNoteX
    gAfterX = SaveGAfterX
    Return
    
'***********************************************

' Find the first valid full 4-part chord after gNoteX.
' Stop searching if at .bFinalChord.
' or if past the .LastNote, when fcnNoteAfter = 0.

GetGAfterX:
    gAfterX = gNoteX
    NoteAfter = fcnNoteAfter(gNoteX) ' gnotex + 1 30sep08
    Do While NoteAfter > 0
        If gNote(NoteAfter).bFinalChord Then
            Exit Do
        ElseIf gNote(NoteAfter).bNHTDelayed Then
            '
        ElseIf gNote(NoteAfter).BestCand > 0 Then
            Exit Do ' a valid full chord
        End If
        gAfterX = NoteAfter ' save the last non-zero one
        NoteAfter = fcnNoteAfter(NoteAfter) ' gafterx + 1 30sep08
    Loop
    If NoteAfter > 0 Then
        gAfterX = NoteAfter
    End If
    Return
    
'***********************************************

CheckNewChords:
    ' Leave:
    '   gSongKey = gNoteX(gAfterX).SongKey
    ' before call cmdChords_Click

    ' Testing Ottman Assn5.4(8) song 13:
    ' Testing Ottman Fig 11.3 song 1
    'GoSub Debug1
    'I = I ' testing
    
    ' generate chords
    ' (1) for a new song if new key; or
    ' (2) if chords changed for this note
    ' with Kn: on RN: card.
    Call cmdChords_Click
    'GoSub Debug1
    'I = I ' testing
    Return

'***********************************************

' NOT USED 30oct08
' because Sub cmdNextSolution changes
'   gCandLast = gNote(gAfterX).LastCand

ReuseCandSlots:
    ' Note that a note's lastcand > firstcand
    ' so reuse the candidate slots starting
    ' as low as possible.
    ' In normal full processing from plastnote to
    ' pfirstnote, gNote(gNoteX).FirstCand =0.
    ' TODO This IF test is needed for cmdNextSolution
    ' and cmdPrevSolution (maybe).
     If gNote(gNoteX).FirstCand > 1 Then
         If gNote(gNoteX).FirstCand - 1 <> _
             gNote(gAfterX).LastCand Then
             I = I ' testing
         End If
         If gNote(gAfterX).LastCand > 0 Then
             gCandLast = gNote(gAfterX).LastCand
         End If
     End If
     Return

'***********************************************

Conclusion:
    ' TODO as a debug feature or option, display
    ' the lowest candidate found, in case
    ' backtracking find a total solution ???

    ' Choose the last note in the song with
    ' candidate chords, and display the
    ' best candidate for that note.
    ' Skip over bar(s) at the end.
    For gNoteX = pLastNote To pFirstNote Step -1
        If gNote(gNoteX).LastCand > 0 Then
            Call DisplayCandChords
            Exit For
        End If
    Next gNoteX
    GoSub DisplayGoBacks
    Return

'***********************************************

DisplayGoBacks:
    E$ = "Harmonize:E5 num gobacks=" & _
        gNumGoBacks & ", gcandlast used=" & _
        gCandLast

    Call DisplayText(E$)
    Debug.Print E$
    Return

'***********************************************
'***********************************************

HarmonizeThisNote:
    'GoSub Test4
    With gNote(gNoteX)
        bFoundMatch = False
        If .bFinalChord Then
            Call FinalTypeChord(bFoundMatch)
            GoSub SaveNoteLinks
            gAfterX = gNoteX ' save for next note
            ' TODO check for failure of Final
        ElseIf gAfterX = 0 Then ' a note must
            ' follow this First/PrevType note
            GoSub Err6
        ElseIf gNote(gAfterX).BestCand = 0 Then
            GoSub MakeItFinal
        Else
            GoSub PrevType
            If .BestCand > 0 Then
                pErr = ""
                GoSub SolutionFound
            Else
                GoSub NoSolutionFound
            End If
        End If
    End With
    'GoSub Test4B
    Return

'***********************************************

Test4:
    If gNoteX = 26 Or gNoteX = 27 Or gNoteX = 28 Then
       I = I
    End If
    Return
    
'***********************************************

Test4B:
    If gNoteX = 26 Or gNoteX = 27 Or gNoteX = 28 Then
       I = I
    End If
    Return
    
'***********************************************
    ' If the note to the right has no
    ' candidates or has all errors,
    ' treat this note as a final, so
    ' that the harmonizing will not
    ' have too many candidate "holes".
MakeItFinal:
    Call FinalTypeChord(bFoundMatch)
    GoSub SaveNoteLinks
    gAfterX = gNoteX ' save for next note
    Return

'***********************************************

    ' Generate candidates for this gNoteX
    ' and pick the best one, .BestCand
PrevType:
    With gNote(gNoteX)
        'GoSub Test1
        gRomNum = NOTSET ' force tonic
        If .bFirstChord Then
            Call FirstTypeChord ' force tonic
            ' next line may not be needed:
            'gAfterX = gNoteX ' save for next note
            ' fall thru to next IF
        End If
        If .bPrevChord Then
            ' forced tonic is bypassed?
            Call PrevTypeChord(bFoundMatch)
            GoSub SaveNoteLinks
            'GoSub Debug1
            
            'call Check_6_3_4
            'call Check_6_4
            'gAfterX = gNoteX
            ' TODO why test both and not
            ' just .BestCand??
            ' TODO 11/21/6
            If .nGoodCand > 0 _
            Or .BestCand > 0 Then
                GoSub Debug1
            End If
        End If
    End With
    Return
    
'***********************************************

Test1:
    If gSongCurr = 1 Then
        If gNoteX = 2 Or gNoteX = 1 Then
            I = I
        End If
    End If
    Return
    
'***********************************************

' For documentation
SaveNoteLinks:
    gNote(gNoteX).AfterX = gAfterX
    gNote(gAfterX).NoteX = gNoteX
    Return

'***********************************************
' Begin of SolutionFound:
'***********************************************

'   Sub PrevTypeChord has found one or more valid
' candidate chords for this note, fulfilling all
' the rules like doubling and harmonic progressions.
' The best candidate is chosen and passed back to
' Sub Harmonize here at SolutionFound:
'   Special processing is done for a note that was
' flagged as a possible non-harmonic tone (NHT).

SolutionFound:
    GoSub Debug2 ' gNoteX
    
    If gNote(gNoteX).xNHT = NHTNONE Then
        gAfterX = gNoteX
    Else
        GoSub NHTSolution
        If NewgAfterX > 0 Then
            gAfterX = NewgAfterX ' gNoteX
        End If
    End If
    
    '.SolutionNo = 0
    If bBackTracking Then
        If ErrorAtgNoteX = gNoteX Then
            bBackTracking = False
            If PassNum = 2 Then
                PassNum = 1
            ElseIf PassNum = 4 Then
                PassNum = 1
                For J = ErrorAtgNoteX To _
                gMaxNextBestNote
                    Bc = gNote(J).BestCand
                    If Left$(gCand(Bc).Error, 1) = "(" Then
                        ' flag this note as allowing
                        ' an error in future e.g. X7th
                        ' TODO may want to be able to
                        ' turn this off, e.g.
                        ' by cmdNextSolution
                        gNote(J).bMustAllowError = True
                    Else
                        gNote(J).bMustAllowError = False
                    End If
                Next J
                gbErrorFourthPass = False
            Else
                Stop
            End If
            ErrorAtgNoteX = 0
            'gMaxNextBestNote = 0
       End If
    End If
    Return

'***********************************************

' In Sub Harmonize, at a solution or non-solution,
' the GoSubs
'   NHTSolution:
'   FindTheNHTHANGCulprit:
' will flag the note as NHTHARMONIC or NHTNONHARM
' and will flag the offending tone or voice in the
' chord as .bNHT = True.
'   The Gosub FindTheNHTHANGCulprit:
' or the Gosub FindTheNHTFIRSTCulprit:
' will add ",N" to the position
' data in grdTable Rows 17-20 for any tone
' flagged as .bNHT = True.
'   The Sub WriteAbc will add a prefix
'   "<(  "">)"^F   if an accidental
'   "<("">)"F      if no accidental.
' to any note tone with ",N" when it writes out
' the ABCPlus music notation data.  This makes the
'   abcm2ps
' program enclose the offending note head
' with parentheses to indicate a NHT tone.

' ASSUMPTION: nonharmonic notes do not extend
' over rests and bars, so the following is used:
'   Note = gNoteX + 1 ' BUMP to next note

NHTSolution:
    GoSub Debug1 ' gNoteX, gAfterX
    If gNoteX = 10 Then
        I = I
    End If
    If gNoteX = 11 Then
        I = I
    End If
    
    If gNote(gNoteX).xNHT And NHTHANG Then
        gNote(gNoteX).xNHT = _
            gNote(gNoteX).xNHT Or NHTHARMONIC 'set
    ElseIf gNote(gNoteX).xNHT And NHTFIRST Then
        gNote(gNoteX).xNHT = _
            gNote(gNoteX).xNHT Or NHTHARMONIC 'set
    Else ' should not happen
        Stop: I = I ' testing
    End If
    
    gNote(gNoteX).xNHT = _
        gNote(gNoteX).xNHT Or NHTFINISHED 'set
    
    GoSub GetNewGAfterX
    GoSub CheckNHTHangChords
    Return

'***********************************************

    ' It the solution note is on a Beat of the
    ' measure, then advance gAfterX.
GetNewGAfterX:
    If gNote(gNoteX).Beat = 0 Then
        NewgAfterX = 0
    Else
        NewgAfterX = gNoteX
    End If
    Return

'***********************************************

    ' Check the NHT Note(s) following gNoteX:
    ' Since we have reached the full chord,
    ' flagged as
    '   xNHT=NHTFIRST,
    ' go back over the following NHTHANG chords
    ' to see which are non-harmonic, not flagged as
    '   xNHT=NHTHARMONIC.
    '   The (non-missing) tone(s) in the
    ' NHTHANG chord that are different from the
    ' previous NHTFIRST chord are the offending
    ' culprit tone(s), which makes the
    ' NHTHANG chord nonharmonic.
    
' WARNING - Do only ONE "AND" BIT test on a BYTE
'           variable (as opposed to a BOOLEAN)
' at a time.
' My convention is to prefix a BYTE variable with 'x'
' and a BOOLEAN variable with 'b'.
'   The result of a bit operation on a BOOLEAN
' variable is True (-1) or False (0).
'   The result of a bit operation on a BYTE variable
' is an integer. False if result=0, else True
' if the result is any other integer.
'   The only way to test more than two BYTE
' operations at a time is to test that each of
' them is false by testing that all together = 0.
'   If you want to test that all of A,B,C are FALSE,
'   IF A AND B AND C = 0
' will work.
    
CheckNHTHangChords:
    Note = gNoteX + 1
    Do While gNote(Note).xNHT And NHTHANG
        If gNote(Note).xNHT And NHTNONHARM Then
            If gNote(Note).xNHT And Not NHTFINISHED Then
                GoSub FindTheNHTHANGCulprit
            End If
        End If
        ' TODO 12/12/6
        If gNote(Note).bFinalChord Then ' DEBUG THIS
            Exit Do
        Else
            Note = Note + 1
        End If
    Loop
    Return

'***********************************************

'   This gNoteX was flagged in StoreTM and ParseAbc
' as NHTFIRST or NHTHANG, part of a group of notes
' one of which may be non-harmonic, that is, with a
' diatonic chord that does not belong to the key.
'   If a note or chord is missing one or more of its
' four tones, which are supplied from a previous
' chord whose tone(s) are prolonged, then this
' "partial chord" (my term) is flagged as
' xNHT=NHTHANG.
'   The "full chord" (my term)
' whose four tones are prolonged into the partial
' chord is flagged as xNHT=NHTFIRST.
'   Several NHTHANG chords may follow a single
' NHTFIRST. At least one of the NHTHANG or NHTFIRST
' chords must be harmonic, flagged as xNHT=NHTHARMONIC,
' and all of them may be harmonic.
'   If a NHFIRST chord is non-harmonic, the
' NHTHANG chord after it should be.  It should be
' considered the substitute for it in all the
' rules like doubling and harmonic progressions.

'   Try to match each tone in the "partial chord" NHTHANG
' note that is not missing with a tone of the same
' letter name in the "full chord" NHTFIRST note.
' The tone or tones that are different must be the
' culprit(s) that are making the chord non-harmonic.
' Flag each offending tone with ",N" in the grdTable in
' Rows 17-20 so that WriteAbc can send it to
'   abcm2ps
' with a special prefix to enclose the note head
' with parentheses.  Usually in a music theory
' notation, a non-harmonic tone in a chord
' has its note head encircled.

' Before 12/12/6
' Per Ottman, I gather that a tone in a partial
' chord, what I call NHTHANG, is non-harmonic
' if it's letter name is not one of the four
' letter names of the last full chord, what
' I call NHTFIRST.  It's pitch can be different
' as long as it's letter name matches one tone.
' If a NHTFIRST note has no valid chord, then some
' NHTHANG note following it should have one.

' Note is the NHTHANG note.
' gNoteX is the NHTFIRST note.

FindTheNHTHANGCulprit:
    GoSub Test2
    
    For V = SOPRANO To BASS
        HangIx = gNote(Note).Voice(V).Index2Name
        If HangIx <> 0 Then
            HangName = gNoteName(HangIx)
            ' Drop the trailing digit, e.g. C#4 -> C#
            HangName = Left$(HangName, Len(HangName) - 1)
            GoSub MatchNHTFirstName
            If Not bFirstNameFound Then
                gNote(Note).Voice(V).bNHT = True
                ' Flag the NHT tone in the grd
                ' Must offset Note by .Firstnote.
                grdTable4P.Col = Note - _
                    gSong(gSongCurr).FirstNote + 1
                grdTable4P.Row = 16 + V
                If Right$(grdTable4P.Text, 1) <> "N" Then
                    grdTable4P.Text = grdTable4P.Text & ",N"
                End If
            End If
        End If
    Next V
    Return

'***********************************************

' For each Voice (or tone) with a non-zero index
' in THIS partial chord NHTHANG,
' look for a matching letter name (plus accidental)
' in the prior full chord NHTFIRST.  If found, this
' tone is harmonic. Ignore a difference in pitch.
MatchNHTFirstName:
    bFirstNameFound = False
    For V2 = SOPRANO To BASS
        FirstIx = gNote(gNoteX).Voice(V2).Index2Name
        If FirstIx = 0 Then
            'Stop:
            I = I ' testing; should not happen
            FirstIx = fcnNoteIndex(gNoteX, V2, bErr) ' call
            FirstIx = Abs(FirstIx) ' minus means user override
            If FirstIx = 0 Then
                Debug.Print "Harmonize:D3 FirstIx=0"
                Stop: I = I ' testing
            End If
        End If
        FirstName = gNoteName(FirstIx)
        ' Drop the trailing digit, e.g. C#4 -> C#
        FirstName = Left$(FirstName, Len(FirstName) - 1)
        If FirstName = HangName Then ' ignore pitch difference
            bFirstNameFound = True
            Exit For
        End If
    Next V2
    Return
    
'***********************************************
    
    ' TODO 12/9/6 not used
        For V = SOPRANO To BASS
        If gNote(Note).Voice(V).Index2Name <> 0 Then
            K = gNote(gNoteX).Voice(V).Index2Name
            If K = 0 Then
                K = fcnNoteIndex(gNoteX, V, bErr) ' call
                K = Abs(K) ' minus means user override
                If K = 0 Then
                    Debug.Print "Harmonize:D4 K=0"
                    Stop: I = I ' testing
                End If
            End If
            'If gNote(gNoteX).Voice(V).Index2Name <>
            If K <> _
                gNote(Note).Voice(V).Index2Name Then
                gNote(Note).Voice(V).bNHT = True
                ' Flag the NHT tone in the grd
                ' Must offset Note by .Firstnote.
                grdTable4P.Col = Note - _
                    gSong(gSongCurr).FirstNote + 1
                grdTable4P.Row = 16 + V
                If Right$(grdTable4P.Text, 1) <> "N" Then
                    grdTable4P.Text = grdTable4P.Text & ",N"
                End If
            End If
        End If
    Next V
    ' End of  12/9/6 not used

'***********************************************
' End of SolutionFound:
'***********************************************
' Begin of NHTSolutionNotFound:
'***********************************************

' No chord was found for this note, not even
' a chord in error.  If the note
' is NHTFIRST or NHTHANG, then mark the note
' as being NHTNONHARM, that is, one or more
' of the four tones is non-harmonic.
NHTSolutionNotFound:
    'bNHTError = False
    If gNote(gNoteX).xNHT And NHTHANG Then
        gNote(gNoteX).xNHT = _
            gNote(gNoteX).xNHT Or NHTNONHARM 'set
        bNHTError = True
        If gNote(gNoteX).Beat > 0 Then
            GoSub NotFoundBackTrack
        End If
    ElseIf gNote(gNoteX).xNHT And NHTFIRST Then
        gNote(gNoteX).xNHT = _
            gNote(gNoteX).xNHT Or NHTNONHARM 'set
        bNHTError = True
        GoSub NotFoundBackTrack
    Else ' should not happen
        Stop: I = I ' testing
    End If
    Return

'***********************************************

    ' Check the NHT Note(s) following gNoteX:
    ' Since we have reached the full chord,
    ' flagged as
    '   xNHT=NHTFIRST,
    ' go back over the following NHTHANG chords
    ' to find the first harmonic one, flagged as
    '   xNHT=NHTHARMONIC.
    ' The (non-missing) tone or tones in the
    ' NHTHANG chord that are different from the
    ' previous NHTFIRST chord are the offending
    ' culprit tones, which makes the
    ' NHTFIRST chord nonharmonic.
NotFoundBackTrack:
    Note = gNoteX + 1
    Do While gNote(Note).xNHT And NHTHANG
        If gNote(Note).xNHT And NHTHARMONIC Then
            NewgAfterX = Note ' reset gAfterX
            GoSub FindTheNHTFIRSTCulprit
            'gNote(Note).xNHT = _
            '    gNote(Note).xNHT Or NHTFIRST 'set
            Exit Do
        End If
        Note = Note + 1
    Loop
    Return
        
'***********************************************

' Before 12/12/6
' Per Ottman, I gather that a tone in a partial
' chord, what I call NHTHANG, is non-harmonic
' if it's letter name is not one of the four
' letter names of the last full chord, what
' I call NHTFIRST.  Its pitch can be different
' as long as its letter name matches one tone.
' If a NHTFIRST note has no chord, then some
' NHTHANG note following it should have one.

' Note is the NHTHANG note.
' gNoteX is the NHTFIRST note.

FindTheNHTFIRSTCulprit:
    GoSub Test2
    
    For V = SOPRANO To BASS
        FirstIx = gNote(gNoteX).Voice(V).Index2Name
        If FirstIx = 0 Then
            'Stop:
            I = I ' testing; should not happen
            FirstIx = fcnNoteIndex(gNoteX, V, bErr) ' call
            FirstIx = Abs(FirstIx) ' minus means user override
        End If
        If FirstIx = 0 Then
            GoSub Debug5
        Else ' If FirstIx <> 0 Then
            FirstName = gNoteName(FirstIx)
            ' Drop the trailing digit, e.g. C#4 -> C#
            FirstName = Left$(FirstName, Len(FirstName) - 1)
            GoSub MatchNHTHangName
            If Not bHangNameFound Then
                gNote(gNoteX).Voice(V).bNHT = True
                ' Flag the NHT tone in the grd
                ' Must offset Note by .Firstnote.
                grdTable4P.Col = gNoteX - _
                    gSong(gSongCurr).FirstNote + 1
                grdTable4P.Row = 16 + V
                If Right$(grdTable4P.Text, 1) <> "N" Then
                    grdTable4P.Text = grdTable4P.Text & ",N"
                End If
            End If
        End If
    Next V
    Return

'***********************************************

Debug5:
    Debug.Print "Harmonize:D5 FirstIx=0 no solution "; _
        "for song,gNoteX, Voice="; _
        gSongCurr; gNoteX; V
    'Stop: I = I ' testing
    Return

'***********************************************

Debug6:
    Debug.Print "Harmonize:D6 HangIx=0 no solution "; _
        "for song,Note, Voice="; _
        gSongCurr; Note; V2
    'Stop: I = I ' testing
    Return

'***********************************************

' For each Voice (or tone) with a non-zero index
' in thIS partial chord NHTHANG,
' look for a matching letter name (plus accidental)
' in the prior full chord NHTFIRST.  If found, this
' tone is harmonic. Ignore a difference in pitch.
MatchNHTHangName:
    bHangNameFound = False
    For V2 = SOPRANO To BASS
        HangIx = gNote(Note).Voice(V2).Index2Name
        If HangIx = 0 Then
            If Note = 116 And V2 = 4 Then
                I = I
            End If
            'Stop: I = I ' testing; should not happen
            HangIx = fcnNoteIndex(Note, V2, bErr) ' call
            HangIx = Abs(HangIx) ' minus means user override
            If HangIx = 0 Then
                GoSub Debug6
            End If
        End If
        HangName = gNoteName(HangIx)
        ' Drop the trailing digit, e.g. C#4 -> C#
        HangName = Left$(HangName, Len(HangName) - 1)
        If FirstName = HangName Then ' ignore pitch difference
            bHangNameFound = True
            Exit For
        End If
    Next V2
    Return

'***********************************************

' TODO 12/12/6 not used
'FindTheNHTFIRSTCulprit:
    If gSongCurr = 3 Then
        I = I ' testing
    End If
    For V = SOPRANO To BASS
        If gNote(Note).Voice(V).Index2Name <> 0 Then
            If gNote(gNoteX).Voice(V).Index2Name <> _
                gNote(Note).Voice(V).Index2Name Then
                gNote(gNoteX).Voice(V).bNHT = True
                ' Flag the NHT tone in the grd
                ' Must offset Note by .Firstnote.
                grdTable4P.Col = gNoteX - _
                    gSong(gSongCurr).FirstNote + 1
                grdTable4P.Row = 16 + V
                If Right$(grdTable4P.Text, 1) <> "N" Then
                    grdTable4P.Text = grdTable4P.Text & ",N"
                End If
            End If
        End If
    Next V
    Return
    
'***********************************************
' End of NHTSolutionNotFound:
'***********************************************

Test5:
    If gNoteX = 1 Then
        I = I
    End If
    Return
    
'***********************************************
' Begin of NoSolutionFound:
'***********************************************

' GoSub NHTSolutionNotFound may set
'   bNHTError = True
' I think this means that if a NHTFIRST note
' had all four tones input but has only
' invalid candidates, then allow the best
' candidate in error for it.
' The NHTHANG note following will probably
' have a valid candidate.
' By comparing the invalid NHTFIRST note
' with a following non-error NHTHANG note,
' the program should be able to flag the
' non-conforming tone in the NHTFIRST note
' by enclosing it in parentheses. 29nov08

NoSolutionFound:
    GoSub Test5
    GoSub Debug1
    
    If gNote(gNoteX).xNHT And NHTFIRST Then
        GoSub CheckAnyNotesInput
        If nTonesInput < 4 Then
            GoTo NoSolutionFound2
        End If
    End If
    
    If gNote(gNoteX).xNHT <> NHTNONE Then
        bNHTError = False
        GoSub NHTSolutionNotFound
        If NewgAfterX > 0 Then
            gAfterX = NewgAfterX ' gNoteX
            NewgAfterX = 0
        End If
        
        If bNHTError Then ' allow any error for NHT chord
            GoSub CheckAnyNotesInput
            If nTonesInput > 0 Then ' 14nov08 test
                GoTo NoSolutionFoundExit
            'ElseIf gNumGoBacks > 100 Then
            ElseIf gNumGoBacks > gNumGoBacksMax Then
                GoTo NoSolutionFoundExit
            End If
        End If
    End If

NoSolutionFound2:
    GoSub BackTracking

NoSolutionFoundExit:
    Return

'***********************************************

' Count the number of voices with an input tone
CheckAnyNotesInput:
    nTonesInput = 0
    For Voice = SOPRANO To BASS
        If gNote(gNoteX).Voice(Voice).Index2Name <> 0 Then
            nTonesInput = nTonesInput + 1
        End If
    Next Voice
    Return
    
'***********************************************

BackTracking:
    'GoSub Debug1
    If Not bBackTracking Then
        If ErrorAtgNoteX = gNoteX Then
            If PassNum = 3 Then
                PassNum = 4
                bBackTracking = True
                gbErrorFourthPass = True
                gNoteX = gNoteX + 1
                Return ' retry the same gnotex solution
                ' but now allowing errors eg X7th
            End If
        ElseIf PassNum = 1 Then
            bBackTracking = True
            PassNum = 2
            ErrorAtgNoteX = gNoteX
            gMaxNextBestNote = 0 ' reset to
            ' calc limit for backtracking
        End If
    End If
    
    GoSub Goback
    If pErr = "NoSol" Then
        If PassNum = 2 Then
            ' redo the original solutions
            ' from gMaxNextBestNote down
            ' to the ErrorAtgNoteX
            ' to prepare for
            ' PassNum=4, which allows
            ' errors e.g. X7th.
            If BackNote > 0 Then
                PassNum = 3
            Else
                ' retry the same gnotex solution
                ' but now allowing errors eg (X7th
                'gNoteX = ErrorAtgNoteX + 1  3/11/08
                gNoteX = fcnNoteAfter(ErrorAtgNoteX)
                gAfterX = gNoteX
                PassNum = 4
                gbErrorFourthPass = True
                Return
            End If
            bBackTracking = False
        ElseIf PassNum = 4 Then ' testing
            Debug.Print "Harmonize:D9 no solution after "; _
                "PassNum="; PassNum; "for gNoteX="; gNoteX
            I = I
            Call DebugNote(gNoteX)
            
            ' TODO 29nov08
            ' WHY is this called for each gNoteX???
            'Stop ' 29nov08 test the need for this loop
            For gNoteX = pLastNote To pFirstNote Step -1
                If gNote(gNoteX).LastCand > 0 Then
                    Call DisplayCandChords
                    Exit For ' 29nov08
                End If
            Next gNoteX
            
            Call DisplayHarm(gSongCurr)
            'GoSub DisplayGoBacks
            GoSub CheckGoBackLimits
            I = I
            'Stop ' 13mar08
        End If
    End If
    Return
    
'***********************************************

Goback:
    GoSub CheckGoBackLimits
    GoSub NextBest
    Return
    
'***********************************************
' NOT USED:
    ' TODO I think this code is not needed:
    If gNote(gNoteX).LastCand > 0 Then
        GoSub NextBest
    ElseIf bFoundMatch Then
        GoSub NextBest
    ElseIf gxAfterFlag And HARMPROG_V_iii6 Then
        GoSub NextBest
    ElseIf gxAfterFlag And HARMPROG_I_vi6 Then
        GoSub NextBest
    ' TODO I don't know if I have to add this line
    ' to match the others above.
    ElseIf gxAfterFlag And HARMPROG_6_3_4 Then
        GoSub NextBest
    Else
        I = I
    End If
    Return
' End of not used:  *******************

'***********************************************

CheckGoBackLimits:
    gNumGoBacks = gNumGoBacks + 1
    'If gNumGoBacks Mod 5000 = 0 Then
    If gNumGoBacks Mod GOBACKSMOD = 0 Then
        Debug.Print "Harmonize:D7 gNumGoBacks="; _
        gNumGoBacks & ", gcandlast used=" & _
        gCandLast
        Call DebugNote(gNoteX)
    End If
    'If gNumGoBacks > 100000 Then
    If gNumGoBacks > gNumGoBacksMax Then
        ' check for runaways
        Debug.Print "Harmonize:D8 gNumGoBacks="; _
        gNumGoBacks & ", gcandlast used=" & _
        gCandLast
        Call DebugNote(gNoteX)
        Stop
        gNumGoBacks = 0 ' 7jan09
    End If
    Return

'***********************************************

NextBest:
    ' Since there are no good candidates for this note
    ' gNoteX, flag the current solution for note
    ' gNoteX+1 as "No Sol".  Then
    ' go to the next solution for note gNoteX+1
    ' or if all used up, back up to the next note with
    ' good candidates up to a limit of four back,
    ' and return the note number in BackNote.
    
    If fcnbNextBest(pFirstNote, pLastNote, BackNote) Then ' call
        'call DebugNote(gAfterX)
        gNoteX = BackNote
        gAfterX = gNoteX ' =Back
        pErr = ""
    Else
        pErr = "NoSol"
        ' BackNote is first note after gMaxNextBestNote
        ' with good candidates.
        If BackNote > 0 Then
            gNoteX = BackNote
            gAfterX = BackNote
        End If
    End If
    Return

'***********************************************
' End of NoSolutionFound:
'***********************************************

Err6:
    E$ = "Harmonize: E6 " & _
        "invalid gAfterX=" & gAfterX & _
        " at gNoteX=" & gNoteX & _
        " at Note=" & gNoteX - pFirstNote + 1 & _
        " in song=" & gSongCurr
    Call DisplayText(E$)
    Debug.Print E$
    'Exit Sub
    'Stop
    Return

'***********************************************

Test2:
    If DebugSong = gSongCurr Then
        If gNoteX >= DebugNote1 _
        And gNoteX <= DebugNote2 Then
            I = I
        End If
    End If
    Return

'***********************************************

Debug1:
    'Return
    If DebugSong = gSongCurr Then
        If gNoteX >= DebugNote1 _
        And gNoteX <= DebugNote2 Then
            Debug.Print "Harmonize:D1 gnotex;gafterx="; _
                gNoteX; gAfterX
            Call DebugNote(gNoteX)
            Call DebugNote(gAfterX)
            Debug.Print
            I = I
        End If
    End If
    Return

'***********************************************

Debug2:
    'Return
    If DebugSong = gSongCurr Then
        If gNoteX >= DebugNote1 _
        And gNoteX <= DebugNote2 Then
            Debug.Print "Harmonize:D2 gnotex;gafterx="; _
                gNoteX; gAfterX
            Call DebugNote(gNoteX)
            I = I
        End If
    End If
    Return

'***********************************************

' Not used:  *******************

Err1:
    Debug.Print "Harmonize:E1 less than"; _
        " two chords in TM: line in song="; _
        gSongCurr
    Exit Sub
Err2:
    Debug.Print "Harmonize:E2 missing "; _
        "double bar || in the TM: line in song="; _
        gSongCurr
    Exit Sub
Err4:
    Dim nDoubleBars As Integer
    Debug.Print "Harmonize:E4 need "; _
        "at least 2 chord notes before the "; _
        "double bar no.="; nDoubleBars; _
        "at note="; I - pFirstNote + 1; _
        "of TM: line in song="; _
        gSongCurr
    Exit Sub
Err3:
    Debug.Print "Harmonize:E3 no solution "; _
        "at note="; I - pFirstNote + 1; _
        "in song="; gSongCurr
    Exit Sub
' End of not used:  *******************

End Sub

Private Sub DisplaySong(pSongno As Integer)
    ' called by sub Main4Part.
    ' called by cmdNextSong_Click.
    ' called by cmdPrevSong_Click.
    
    ' calls sub InitKey

    Dim Counter As Integer
    Dim I As Integer
    Dim J As Integer
    Dim Key(2) As String
    Dim A$

    'If gSongCurr = 9 Then
    '    I = I
    'End If
    If pSongno <= 0 Or pSongno > gSongLast Then
        Debug.Print "DisplaySong:E1 "; _
            "invalid SongNo="; pSongno
    Else
        With gSong(pSongno)
            txtSongNum.Text = .X
            txtDesc.Text = gFile & ": " & .T
            txtMeter.Text = .M.Text
            txtLength.Text = .L.Text & " " & .L.UText
            grdKey.Col = 0
            For Counter = 0 To grdKey.Rows - 1
                grdKey.Row = Counter
                grdKey.Text = ""
                grdKey.ColWidth(0) = 855
            Next Counter
            GoSub Keytable
            ' TODO drop .NumKey from gSong
            'For Counter = 0 To .NumKey - 1
            For Counter = 0 To 2
                grdKey.Row = Counter
                grdKey.Text = Key(Counter)
            Next Counter
            'call InitKey ' 05may08
            grdKeySig.Col = 0
            For Counter = 0 To grdKeySig.Rows - 1
                grdKeySig.Row = Counter
                grdKeySig.Text = ""
            Next Counter
            I = gSong(gSongCurr).KeySigUsedIst - 1
            J = grdKeySig.Rows - 1
            If I < J Then
                J = I
            End If
            'For Counter = 0 To grdKeySig.Rows - 1
            For Counter = 0 To J
                grdKeySig.Row = Counter
                grdKeySig.Text = _
                    gSong(gSongCurr).KeySigIst(Counter + 1)
            Next Counter
        
        End With
    End If
    Exit Sub

Keytable:
    With gSong(pSongno)
        ' Add a blank to fixed string *2
        ' gsong().key().name so that
        ' it can be trimmed to a normal string.
        A$ = Trim$(.Key(1).Name & " ")
        Key(0) = A$
        'Key(0) = .Key(1).Name
        If .Key(1).Mode = MAJOR Then
            Key(1) = "MAJOR"
            Key(2) = ""
        Else
            Key(1) = "MINOR"
            If .Key(1).Mode = MINORNAT Then
                Key(2) = "NATURAL"
            ElseIf .Key(1).Mode = MINORHARM Then
                Key(2) = "HARMONIC"
            Else
                Key(2) = "MELODIC"
            End If
        End If
    End With
    Return
End Sub


Private Sub DisplayNotes(pSongno As Integer)
    ' called by sub Main4Part
    ' called by sub cmdNextSong_Click.
    ' called by sub cmdPrevSong_Click.
    ' called by sub cmdShow_Click.
    ' calls function fcnHideKeyAccidentals
    
    Dim Note As Integer
    Dim J As Integer
    Dim K As Integer
    Dim Meas As Integer
    Dim A$
    Dim B$
    Dim V As Integer
    Dim V2 As Integer

'****************************************
'****************************************
    
Main:
        GoSub Init
        GoSub MainLine
        Exit Sub
    
'****************************************
'****************************************
    
Init:
    Return
    
'****************************************
    
MainLine:
    Meas = 1
    If pSongno <= 0 Or pSongno > gSongLast Then
        Debug.Print "DisplaySong:D1 "; _
            "invalid SongNo="; pSongno
    Else
        With gSong(pSongno)
            grdTable4P.Cols = .LastNote - .FirstNote + 2
            Call cmdClear_Click ' clear grid to blanks
            grdTable4P.FillStyle = grdSingle ' single cells
            Call SetGrid4PartLabels ' reset col num's
            '
            grdTable4P.Col = 0
            For Note = .FirstNote To .LastNote
                If Note = 6 Then
                    J = J
                End If
                GoSub CopyRests
                'GoSub CopySlurs
                'GoSub CopyTies
                grdTable4P.Col = grdTable4P.Col + 1
                grdTable4P.Row = 1
                A$ = gNote(Note).Voice(SOPRANO).Name
                ' Hide the # or b accidentals that are
                ' in the key signature:
                grdTable4P.Text = fcnHideKeyAccidentals(A$)
                
                ' Mark the measure no.s at the bars
                ' of the soprano line.
                A$ = gNote(Note).TMName
                If Left$(A$, 1) = "|" Then
                    Meas = Meas + 1
                    'grdTable4P.Text = _
                    '    grdTable4P.Text _
                    '    & "m=" & Format(Meas)
                     If Meas > 99 Then
                         grdTable4P.Text = _
                             A$ & "m" & Format(Meas)
                     Else
                         grdTable4P.Text = _
                             A$ & " m=" & Format(Meas)
                     End If
                End If
                
                grdTable4P.Row = 2
                A$ = gNote(Note).Voice(ALTO).Name
                grdTable4P.Text = fcnHideKeyAccidentals(A$)
                
                grdTable4P.Row = 3
                A$ = gNote(Note).Voice(TENOR).Name
                grdTable4P.Text = fcnHideKeyAccidentals(A$)
                
                grdTable4P.Row = 4
                A$ = gNote(Note).Voice(BASS).Name
                grdTable4P.Text = fcnHideKeyAccidentals(A$)
                
                grdTable4P.Row = 5
                A$ = gNote(Note).TMName
                grdTable4P.Text = LCase$(A$)
                
                'GoSub Bars
                grdTable4P.Row = 6
                grdTable4P.Text = gNote(Note).RNName
                
                grdTable4P.Row = 7
                grdTable4P.Text = gNote(Note).FBName
                ' Warn that the figured bass inversion
                ' may be wrong in case the Bass is
                ' Crossed with other note. 3aug08
                If gNote(Note).bCrossedBass Then
                    grdTable4P.Text = grdTable4P.Text & "?"
                End If
                
                If Note < .LastNote Then
                    grdTable4P.Row = 8
                    If Left$(A$, 1) = "|" Then
                        grdTable4P.Text = ""
                    Else
                        grdTable4P.Text = "0"
                    End If
                End If
                If gbOneLyricLinePerVoice Then
                    GoSub CopyLyricsOnePerVoice
                Else
                    GoSub CopyLyrics
                End If
                GoSub CopyRests2
                GoSub Bars
                GoSub CopyT1toT4
            Next Note
        End With
        'for i=0 to 5:?gSong(pSongNo).KY(Note);:next Note
    End If
    Return
    'Exit Sub
    
'****************************************
    
' 11/29/6 To make rooom at line 12 and 13 for Outer/Inner
' and NHT, I moved Lyric1 and Lyric2 to 14-15 and
' I moved Lyric3 and Lyric4 to 23-24.
' I think that it may be tricky to move the
' TM: lines 17-20, so I will do that later.

' voice 1 has lyric lines 1, 2, 3, 4

'****************************************

CopyLyrics:
    V = SOPRANO ' 31jul08
    J = 0
    With gNote(Note)
        'For K = grdTable4P.Rows - 4 To grdTable4P.Rows - 1
        For K = 14 To 15
            J = J + 1
            grdTable4P.Row = K
            grdTable4P.Text = gNote(Note).Voice(V).Lyric(J)
        Next K
        For K = 23 To 24
            J = J + 1
            grdTable4P.Row = K
            grdTable4P.Text = gNote(Note).Voice(V).Lyric(J)
        Next K
    End With
    GoSub CopyNht
    Return

' voice 1 has lyric line 1, voice 2 has lyric line 2, ...

'****************************************

CopyLyricsOnePerVoice:
    J = 1
    V = 0 ' 1=Soprano, 4=Bass
    With gNote(Note)
        'For K = grdTable4P.Rows - 4 To grdTable4P.Rows - 1
        For K = 14 To 15
            V = V + 1
            grdTable4P.Row = K
            grdTable4P.Text = gNote(Note).Voice(V).Lyric(J)
        Next K
        
        For K = 23 To 24
            V = V + 1
            grdTable4P.Row = K
            grdTable4P.Text = gNote(Note).Voice(V).Lyric(J)
        Next K
    End With
    GoSub CopyNht
    Return

'****************************************

CopyNht:
    grdTable4P.Row = 13
    If gNote(Note).xNHT And NHTFIRST Then
        grdTable4P.Text = "Nht F"
    ElseIf gNote(Note).xNHT And NHTHANG Then
        grdTable4P.Text = "Nht H"
    ElseIf gNote(Note).xNHT > 0 Then
        Debug.Print "DisplayNotes:D2 xNHT="; gNote(Note).xNHT
        Stop: K = K ' testing
    End If
    Return

'****************************************

' Assume if a rest is found in a voice, that all
' the other voice also have a rest.
' For writing out the ABC file,
' copy the "z" to the rest of the rows in that col.
CopyRests:
    With gNote(Note)
        For J = SOPRANO To BASS
            If .Voice(J).xRest And RESTALL4 Then
                For K = SOPRANO To BASS
                    If .Voice(K).Name = "" Then
                        '.Voice(K).Name = UCase$("z")
                        .Voice(K).Name = "z"
                    End If
                Next K
            End If
        Next J
    End With
    Return

'****************************************

CopyRests2:
    With gNote(Note)
        'If UCase$(.Voice(1).Name) = "z" Then
        If LCase$(.Voice(1).Name) = "z" Then
            For K = BASS + 2 To grdTable4P.Rows - 1
                If K = 14 Or K = 15 _
                Or K = 23 Or K = 24 _
                Or K = 21 Then
                    ' skip lyric lines and Bass4 line
                Else
                    grdTable4P.Row = K
                    grdTable4P.Text = "*"
                End If
            Next K
        End If
    End With
    Return

'****************************************

' For writing out the ABC file,
' copy the bars to all the rows in that col.
Bars:
    A$ = gNote(Note).TMName
    If A$ = "|" Or A$ = "||" Then
        For J = SOPRANO To BASS
            grdTable4P.Row = J
            If grdTable4P.Text = "" Then
                grdTable4P.Text = "   " & A$
            End If
        Next J
        For J = BASS + 2 To grdTable4P.Rows - 1
            grdTable4P.Row = J
            grdTable4P.Text = "|"
        Next J
    End If
    Return

'****************************************

' Copy the time name and position in the measure
' for T1: to T4:, corresponding to V1: to V4:.
CopyT1toT4:
    With gNote(Note)
        J = 0
        For K = 17 To 20
            J = J + 1 ' 1 to 4
            grdTable4P.Row = K
            If grdTable4P.Text <> "|" Then
            With gNote(Note).Voice(J).Time
                If .Offset < 0 Then
                    grdTable4P.Text = _
                        .Name & "," & .Pos & _
                        "," & .Offset
                Else
                    grdTable4P.Text = _
                        .Name & "," & .Pos
                End If
            End With
            End If
        Next K
    End With
    Return

End Sub


Private Sub DisplayHarm(pSongno As Integer)
    ' called by sub cmdHarmony_Click
    ' called by sub cmdNextSolution_Click
    ' called by sub cmdPrevSolution_Click
    ' called by sub NewBestCandidate
    ' called by sub Harmonize
    
    ' (Called by sub cmdNextSong_Click)
    ' (Called by sub cmdPrevSong_Click)
    ' (Called by sub cmdShow_Click)

    ' calls sub cmdChords_Click
    ' calls function fcnRepChar
    ' calls function fcnReplace
    ' calls function fcnNoteIndex
    
    ' Globals used: gSong()..., gCand(Cand).Index2Ch,
    '       gChord()..., gNote(Note).BestCand
    ' Globals set:
    ' Note- if a string without blanks cannot
    '       fit wholely in a grid square, then
    '       it displays as blank.
    '       E.g. B5 & "G#2" shows as blank
    '       but  B4 & " G#2" shows ok.
    Const B1 As String = " "
    Const B2 As String = "  "
    Const B3 As String = "   "
    Const B4 As String = "    "
    Const B5 As String = "     "

    Dim Note As Integer
    Dim Cand As Long
    Dim Chord As Integer
    Dim I As Integer
    Dim L As Integer
    Dim J As Integer
    Dim K As Integer
    Dim Voice As Integer
    Dim Z As Single
    Dim A$
    Dim B$
    Dim E$
    Dim Incr As Integer
    Dim UnSorted(1 To 6) As Integer
    Dim Sorted(1 To 6) As Integer
    ' For ShowRomNum:
    Dim Key$
    Dim Key2$
    Dim User1$
    Dim Blnk$
    Dim Chrd$
    Dim KeyNo As Integer
    'Dim SaveRow As Integer
    Dim Modes As Variant
    Dim bErr As Boolean
    Dim NextGAfterX As Integer
    Dim LParen$
    Dim RParen$
    Dim BadError$
    Dim BadErrorNote As Integer

    Const WIDECHARS As String = "ABCDEFGmV7="
    Const RnLenMax As Integer = 10
    Dim nWide As Integer
    Dim RnLen As Integer
    Dim VSlash As String
    Dim V As Integer
    Dim V2 As Integer
    Dim Offset1 As Integer

'******************************************
'******************************************
    
Main:
    GoSub Init
    GoSub MainLine
    If BadError$ <> "" Then
        GoSub Err4
    End If
    Exit Sub

'******************************************
'******************************************

Init:
    If pSongno <= 0 Or pSongno > gSongLast Then
        GoTo Err1
    End If
    Return
    
'******************************************

MainLine:
    With gSong(pSongno)
        'grdTable4P.Cols = .LastNote - .FirstNote + 2
        grdTable4P.Col = 0
        For Note = .FirstNote To .LastNote
            If Note = 46 Then
                Note = Note
            End If
            If gNote(Note).bBar _
            Or gNote(Note).bRestAll4 Then
                GoTo Next1
            End If
            Cand = gNote(Note).BestCand
            If Cand = 0 Then
                If Note = 164 Then
                    I = I
                End If
                grdTable4P.Col = Note + 1 - .FirstNote
                For V = SOPRANO To BASS ' 7jan09
                    grdTable4P.Row = V
                    grdTable4P.Text = ""
                Next V
                grdTable4P.Row = 6
                grdTable4P.Text = "(..)" ' 7jan09
                ' TODO
                'If Left$(grdTable4P.Text, 2) <> "(." Then
                '    A$ = grdTable4P.Text
                '    A$ = fcnRepChar(A$, "(", "")
                '    A$ = fcnRepChar(A$, ")", "")
                '    grdTable4P.Text = "(." _
                '        & Trim$(A$) & ".)"
                'End If
            Else
                GoSub DisplayBestCand
            End If
Next1:
        Next Note
    End With
    Return
    'Exit Sub
    'for i=0 to 5:?gSong(pSongNo).KY(i);:next i

'******************************************

DisplayBestCand:
    gSongKey = gNote(Note).SongKey
    'call cmdChords_Click
    ' see if chords changed
    Chord = gCand(Cand).Index2Ch
    grdTable4P.Col = Note + 1 - _
        gSong(pSongno).FirstNote
    grdTable4P.Row = 16
    grdTable4P.Text = Str$(Chord)
    ' Can use this fcn
    '   fcnNote2Col(Note)
    ' since all calls to DisplayHarm
    ' use gSongCurr as yet.
    'grdTable4P.Col = fcnNote2Col(Note)
    For Voice = SOPRANO To BASS ' 1 to 4
        GoSub Voices
    Next Voice
    GoSub CopySlurs
    GoSub CopyTies
    GoSub ShowRomNum
    GoSub ShowFigBass
    GoSub ShowVarRank
    GoSub ShowErrorOrCands
    GoSub ShowInterval
    'GoSub ShowInterval2
    GoSub ShowOuterInner
    GoSub ShowGAfterX
    Return

'******************************************

CopySlurs:
    With gNote(Note)
        For V = SOPRANO To BASS
            If .Voice(V).xSlur And SLURALL4 Then
                'SaveRow = grdTable4P.Row
                For V2 = SOPRANO To BASS
                    'If V2 <> V Then
                        GoSub SlurLoop
                    'End If
                Next V2
                Exit For
                'grdTable4P.Row = SaveRow
            ElseIf .Voice(V).xSlur And SLURBEG Then
                'GoSub CheckSlurBeg
                V2 = V
                GoSub SlurLoop
            ElseIf .Voice(V).xSlur And SLUREND Then
                'GoSub CheckSlurEnd
                V2 = V
                GoSub SlurLoop
            End If
        Next V
    End With
    Return

'******************************************
    
' Add "(" if .Name does not have it
' due to OP: MISC COMMENT V:1 etc
CheckSlurBeg:
    With gNote(Note).Voice(V)
        If Left$(LTrim$(.Name), 1) <> "(" Then
            V2 = V
            GoSub SlurLoop
        End If
    End With
    Return
    
'******************************************

' Add ")" if .Name does not have it
' due to OP: MISC COMMENT V:1 etc
CheckSlurEnd:
    With gNote(Note).Voice(V)
        If Right$(RTrim$(.Name), 1) <> ")" Then
            V2 = V
            GoSub SlurLoop
        End If
    End With
    Return
    
'******************************************

SlurLoop:
    With gNote(Note)
        grdTable4P.Row = V2
        A$ = Trim$(grdTable4P.Text)
        If .Voice(V2).xSlur And SLURBEG Then
            If Left$(A$, 1) <> "(" Then
                I = .Voice(V2).OpenSlurs
                If I > 2 Then
                    I = I
                End If
                If I = 0 Then
                    I = 1
                End If
                B$ = Left$("((((((((", I)
                If Left$(grdTable4P.Text, 1) = " " Then
                    grdTable4P.Text = B4 & B$ & A$
                Else
                    grdTable4P.Text = B$ & A$
                End If
            End If
        ElseIf .Voice(V2).xSlur And SLUREND Then
            If Right$(A$, 1) <> ")" Then
                I = .Voice(V2).CloseSlurs
                If I = 0 Then
                    I = 1
                End If
                B$ = Left$("))))))))", I)
                grdTable4P.Text = A$ & B$
            End If
        End If
    End With
    Return

'******************************************

CopyTies:
    With gNote(Note)
        For J = SOPRANO To BASS
            If .Voice(J).xTie And TIEALL4 Then
            'If .Voice(J).xTie And TIE Then ' 4jan09
                For K = SOPRANO To BASS
                    grdTable4P.Row = K
                    If Right$(grdTable4P.Text, 1) <> "-" Then
                        grdTable4P.Text = _
                            RTrim$(grdTable4P.Text) _
                            & "-"
                    End If
                Next K
                Exit For
            End If
        Next J
    
        For J = SOPRANO To BASS
            If .Voice(J).xTie And TIE Then ' 4jan09
                    grdTable4P.Row = J
                    If Right$(grdTable4P.Text, 1) <> "-" Then
                        grdTable4P.Text = _
                            RTrim$(grdTable4P.Text) _
                            & "-"
                    End If
            End If
        Next J
    
    End With
    Return

'******************************************

' If no note was input for this voice,
' then set the text in the grdTable to
' the note from the calculated four-part
' harmony, right-adjusted.  The input notes
' will stay Left$-adjusted to identify
' themselves.
Voices:
    grdTable4P.Row = Voice

    ' TODO cleanup
    If gNote(Note).Voice(Voice).Index2Name = 0 Then
    'If fcnNoteIndex(Note, Voice, bErr) = 0 Then
        ' TODO what is the purpose of this?  It
        ' is not used in Dies Irae:
        ' Possibly a single left paren is used
        ' to indicate beginning of a phrase where
        ' no note was input for that voice.
        
        ' A negative Offset1 indicates a NHTHang
        ' note following some NHTFIRST note, so
        ' do not fill in  and display the tones from
        ' the NHTFIRST note.
        If gNote(Note).Voice(Voice).Name <> "z" Then
            Offset1 = _
                gNote(Note).Voice(Voice).Time.Offset
            If Offset1 >= 0 Then
                If Left$(grdTable4P.Text, 1) = "(" Then
                    'grdTable4P.Text = "" ' 14nov08
                End If
                L = gChord(Chord).Voice(Voice).Index2Na
                A$ = gNoteName(L)
                grdTable4P.Text = B4 & _
                    fcnHideKeyAccidentals(A$)
            End If
        End If
    ElseIf gNote(Note).Voice(Voice).Index2Name < 0 Then
    'ElseIf fcnNoteIndex(Note, Voice, bErr) < 0 Then
        ' TODO what is this from? User override, I think.
        A$ = gNote(Note).Voice(Voice).Name
        grdTable4P.Text = fcnHideKeyAccidentals(A$)
    End If

    Return

'*************************************************

' Left$-adjust the Rom num in the grd text field
' if it was entered by the user.
'   If it was calculated by the program, insert
' 3-4 blanks before it, since the grid table
' doesn't support right-adjustment for a single
' slot.
'   If a modulation key field, e.g. K2:,
' precedes the Rom num, insert 1-2 blanks before it.
'   If a note was altered by #3 or b6, treat it
' as calculated by the program.
' A modulation key, e.g. K2:, is carried in each
' following note's .SongKey until it is turned off.

' Enclose the RomNum within parentheses if it is
' a NHT, except if a Harmonic NHT note follows
' a Beat note which does not have a chord solution.
' See Sub Harmonize.

ShowRomNum:
    grdTable4P.Row = 6
    User1$ = gNote(Note).RNName ' entered by user
    KeyNo = gNote(Note).SongKey ' 1=K1:, 2=K2:,...
    'GoSub Test1
    If KeyNo > 1 Then
        'Key2$ = "K" & Chr$(48 + KeyNo) & ":"
        GoSub GetNewKeyName
        Key2$ = A$
    Else
        Key2$ = ""
    End If
    If User1$ = "" Then ' no entry by user
        Key$ = Key2$
        Blnk$ = " "
        Chrd$ = gChord(Chord).RomNumName
    Else
        If Left$(User1$, 1) = "K" Then
            If Len(User1$) > 3 Then
                ' A modulation key, e.g. K2:, was
                ' followed by a user entered RomNum.
                Key$ = Left$(User1$, 3) ' K2:vi, K1:I
                GoSub EditKeyName
                Key$ = A$
                User1$ = Mid$(User1$, 4)
                Blnk$ = ""
                Chrd$ = ""
            Else ' Only a modulation key, e.g. K2:, K1:
                Key$ = Left$(User1$, 3)
                GoSub EditKeyName
                Key$ = A$
                User1$ = ""
                Blnk$ = " "
                Chrd$ = gChord(Chord).RomNumName
            End If
        Else ' user entered some RomNum
            Key$ = Key2$
            Blnk$ = ""
            Chrd$ = gChord(Chord).RomNumName
            If Chrd$ = User1$ Then
                Chrd$ = ""
            ElseIf Chrd$ <> "" Then
                User1$ = User1$ & " => "
            End If
        End If
    End If
    
    GoSub AdjustRNDisplay
    'GoSub AddParens
    'grdTable4P.Text = LParen$ & A$ & RParen$
    grdTable4P.Text = A$
    'If gCand(Cand).VSlash <> "" Then
        'grdTable4P.Text = grdTable4P.Text & _
            "=" & gCand(Cand).VSlash ' 20aug08
    'End If
    Return

'*************************************

Test1:
    If Note = 7 Then
        I = I
    End If
    If User1$ = "K2:" Then
        I = I
    End If
    If KeyNo = 2 Then
        I = I
    End If
    
    ' TODO
    If User1$ = "i" Then
        I = I
    End If
    Return

'*************************************

EditKeyName:
    KeyNo = Val(Mid$(Key$, 2, 1))
    If KeyNo < 1 Then
        Stop
    ElseIf KeyNo > NUMKEYMAX Then
        Stop
    Else
        GoSub GetNewKeyName
    End If
    Return

'******************************************

'M=maj,m=min,d=dorian,phrygian,l=lydian,x=mixolydian
GetNewKeyName:
    Modes = Array("", "", "m", "d", "p", "l", "x")
    A$ = gSong(gSongCurr).Key(KeyNo).Name
    A$ = Trim$(A$ + " ") ' add blank to trim fixed string
    J = gSong(gSongCurr).Key(KeyNo).Mode
    If J >= 2 And J <= 6 Then
        A$ = A$ & Modes(J) & ":"
    Else
        A$ = A$ & ":"
    End If
    Return

'******************************************

' VSlash is "V/" or "V7/" if the note is a Secondary
' Dominant to the gAfterX note.

' Count the number of wide letters or chars
'   A B C D E F G V 7 =
' in the RomNum field and reduce the length
' by 1 for each one found, if needed:

'
AdjustRNDisplay:
    GoSub AddParens
    VSlash = gCand(Cand).VSlash ' 20aug08
    If gCand(Cand).VSlash = "" Then
        A$ = LParen$ & Key$ & User1$ & Blnk$ _
            & Chrd$ & RParen$
    Else
        A$ = LParen$ & Key$ & User1$ & Blnk$ _
            & Chrd$ & "=" & VSlash & RParen$
    End If
    GoSub CountWideChars
    RnLen = RnLenMax - nWide
    If Len(A$) > RnLen Then
        GoSub DropSubFields
    End If
    If Len(A$) < RnLen Then
        GoSub AddBlanks
    End If
    Return

'******************************************
' (E:V7/)
' Em:V7/
' Em:vii o
' NOT USED 21aug08
    If Note >= 6 And Note < 8 Then
        I = I
    End If
    If User1$ = "K2:" Then
        I = I
    End If
    If Key$ = "K2:" Then
        I = I
    End If
    'If Left$(A$, RNLEN) = "(G:vii" Then
    '    I = I
    'End If
    'If A$ = "(.K2:.)" Then
     '   I = I
    'End If

'******************************************

AddParens:
    If gNote(Note).xNHT And NHTHANG _
    And Note < NextGAfterX Then
        LParen$ = "("
        RParen$ = ")"
    Else
        LParen$ = ""
        RParen$ = ""
        NextGAfterX = gNote(Note).AfterX
    End If
    Return
    
'*************************************************

CountWideChars:
    nWide = 0
    For I = 1 To Len(A$)
        J = InStr(WIDECHARS, Mid$(A$, I, 1))
        If J > 0 Then
            nWide = nWide + 1
        End If
    Next I
    Return
    
'*************************************************

' Order of fields to drop to fit in RnLen:
'   Blnk$, B$, Key$, both Key$ and B$
' Finally, truncate to RnLen.

DropSubFields:
    A$ = fcnRepChar(A$, " ", "") ' call
    If Len(A$) > RnLen Then
        B$ = User1$ & Chrd$ & "="
        A$ = LParen$ & B$ & VSlash & RParen$
        If Len(A$) > RnLen Then
            A$ = LParen$ & Key$ & VSlash & RParen$
            If Len(A$) > RnLen Then
                A$ = LParen$ & VSlash & RParen$
                If Len(A$) > RnLen Then
                    A$ = Left$(A$, RnLen)
                End If
            End If
        End If
    End If
    Return
    
'*************************************************

AddBlanks:
        If Blnk$ = " " Then
            Blnk$ = Blnk$ & Space$(RnLen - Len(A$))
            'A$ = Key$ & User1$ & Blnk$ & Chrd$
            If Len(Blnk$) > 1 Then
                A$ = fcnReplace(A$, " ", Blnk$)
            End If
        End If
        Return
    
'*************************************************

ShowFigBass:
    'grdTable4P.Row = 7
    'If grdTable4P.Text = "" Then
    If gChord(Chord).FBName = "5 3" Then
        A$ = ""
    Else
        A$ = gChord(Chord).FBName
    End If
    grdTable4P.Row = 7
    If gNote(Note).bAlterAboveBass Then
        grdTable4P.Text = _
            gNote(Note).AlterAboveBassNM & "," & A$
    ElseIf gNote(Note).bAlterSDeg Then
        grdTable4P.Text = _
            gNote(Note).AlterSDegNM & "," & A$
    ElseIf gNote(Note).FBName = "" Then
        grdTable4P.Text = B5 & A$
    Else
        grdTable4P.Text = A$ ' entered by user
        ' insurance check:
        If A$ <> gNote(Note).FBName Then
            'GoSub Err3
            grdTable4P.Text = B5 & A$
        End If
    End If
    
    ' Warn that the figured bass inversion
    ' may be wrong in case the Bass is
    ' Crossed with other note. 3aug08
    If gNote(Note).bCrossedBass Then
        grdTable4P.Text = grdTable4P.Text & "?"
    End If
    Return

'******************************************

ShowVarRank:
    grdTable4P.Row = 8
    Z = gCand(Cand).Variance
    B$ = Str$(Z)
    If InStr(B$, ".") Then
        B$ = B$ & "/"
    Else
        B$ = B$ & " / "
    End If
    If Z > -1 Then
        grdTable4P.Text = B$ _
        & gCand(Cand).Rank
    Else
        grdTable4P.Text = _
        "/  " & gCand(Cand).Rank
    End If
    Return

'******************************************

' If no error text to display, display
' the number of valid candidates for this note.
ShowErrorOrCands:
    grdTable4P.Row = 9
    If gCand(Cand).Error = "" Then
        A$ = Format(gNote(Note).nGoodCand)
        If Len(A$) < 8 Then
            grdTable4P.Text = _
                Space$(8 - Len(A$)) & A$
            Else
                grdTable4P.Text = A$
        End If
    Else
        grdTable4P.Text = _
            gCand(Cand).Error
        ' Save rightmost "bad" error for err msg Err4.
        If Left$(gCand(Cand).Error, 1) <> "(" Then
            BadError$ = gCand(Cand).Error
            BadErrorNote = _
                Note - gSong(pSongno).FirstNote + 1
        End If
    End If
    Return

'******************************************

' Show unsorted gChord(k).interval(1 to 6)
' TODO Should change labels in sub SetGrid4PartLabels.
ShowInterval:
    grdTable4P.Row = 10
    grdTable4P.Text = _
        gChord(Chord).Interval(1) & " " & _
        gChord(Chord).Interval(2) & " " & _
        gChord(Chord).Interval(3)
    grdTable4P.Row = 11
    grdTable4P.Text = _
        gChord(Chord).Interval(4) & " " & _
        gChord(Chord).Interval(5) & " " & _
        gChord(Chord).Interval(6)
    Return

'******************************************

' Show sorted gChord(k).interval(1 to 6)
ShowInterval2:
    For I = 1 To 6
        UnSorted(I) = gChord(Chord).Interval(I)
    Next I
    For I = 1 To 6
        Sorted(I) = fcnMinArray(1, 6, UnSorted())
    Next I
    grdTable4P.Row = 10
    grdTable4P.Text = _
        Sorted(1) & " " & _
        Sorted(2) & " " & _
        Sorted(3)
    grdTable4P.Row = 11
    grdTable4P.Text = _
        Sorted(4) & " " & _
        Sorted(5) & " " & _
        Sorted(6)
    Return

'******************************************

' If the chord for this best cand is >
'   UPPERCHORDSMIN (= 8000)
' then enclose .OuterGraphic in square brackets
' to indicate that a candidate had to be
' made for a non-valid chord in the Upper
' Chord part.  This helps the user check
' out these non-valid chords.

ShowOuterInner:
    grdTable4P.Row = 12
    'grdTable4P.Text = gCand(Cand).OuterGraphic _
    '    & "     " & gCand(Cand).InnerGraphic
    grdTable4P.Text = gCand(Cand).OuterGraphic
    If Chord > UPPERCHORDSMIN Then
        grdTable4P.Text = _
            "  [" & grdTable4P.Text & "]"
    End If
    Return

'******************************************

ShowGAfterX:
    grdTable4P.Row = 22
    grdTable4P.Text = gNote(Note).AfterX
    Return

'******************************************

Err1:
    Debug.Print "DisplayHarm:E1 "; _
        "invalid SongNo="; pSongno
    Stop

'******************************************

Err2:
    Debug.Print "DisplayHarm:E2 "; _
        "x exceeds grdTable4P.Cols in SongNo="; pSongno
    Stop

'******************************************

Err3:
    Debug.Print "DisplayHarm:E3 "; _
        "SongNo="; pSongno; _
        " Cand FB="; gChord(Chord).FBName; _
        " <> User FB ="; gNote(Note).FBName
    I = I
    Return

Err4:
    E$ = "DisplayHarm:E4 SongNo=" & pSongno _
        & " non-allowed error=" & BadError$ _
        & " at note=" & BadErrorNote
    Debug.Print E$
    Call DisplayText(E$)
    Return

End Sub

Private Sub cmdAlign_Click()
    ' (NOT USED ANY MORE)
    ' called by cmd click
    Dim ColCounter

    txtPrint.Text = ""
    ' Center the text in the cells.
    For ColCounter = 1 To (grdTable4P.Cols - 1) Step 1
        grdTable4P.ColAlignment(ColCounter) = 2
    Next

    'grdTable4P.FixedAlignment(0) = 2
    'grdTable4P.FixedAlignment(1) = 1
    'grdTable4P.FixedAlignment(2) = 0 '3 is invalid

End Sub


Private Sub cmdClear_Click()
    ' called by cmd click
    ' called by sub DisplayNotes
    ' Note that all the cells fill with dark blue.
    ' When you click in any cell, all the cells
    ' turn to white.  When you click and drag,
    ' you turn the selected cells to dark blue.

    txtPrint.Text = ""
    Call ClearTable4P
    'call ClearExtra
End Sub

Private Sub ClearTable4P()
    ' called by sub cmdClear_Click

    ' Note that all the cells fill with dark blue.
    ' When you click in any cell, all the cells
    ' turn to white.  When you click and drag,
    ' you turn the selected cells to dark blue.

    ' Select from Row #1, Col #1
    grdTable4P.SelStartCol = 1
    grdTable4P.SelStartRow = 1
    ' End selection at bottom right cell
    grdTable4P.SelEndCol = grdTable4P.Cols - 1
    grdTable4P.SelEndRow = grdTable4P.Rows - 1

    ' Set FillStyle to 1 (fill them all).
    grdTable4P.FillStyle = grdRepeat ' 1

    ' Fill all the cells with null.
    grdTable4P.Text = " "

End Sub



Private Sub cmdExit_Click()
    ' called by cmd click
    End ' No more cmd buttons can be used.
End Sub
Private Sub cmdHarmony_Click()
    ' called by sub Main4Part
    ' called by sub cmdNextSong_Click
    ' called by sub cmdPrevSong_Click
    ' called by sub cmdShow_Click
    ' called by cmd click

    ' calls sub InitProgressions
    ' calls sub InitErrorAllow
    ' calls sub WriteAllChords
    ' calls sub InitVoiceRange ' once per song
    ' calls sub SetNoteFlags
    ' calls sub Harmonize
    ' calls sub DisplayHarm

    ' Globals used: gSong()..., gNote(gNoteX)...
    ' Globals set: gNote(I).BestCand = 0, gNoteX =

    ' Process a new song

    Dim I As Integer
    Dim J As Integer
    Dim FirstNote As Integer
    Dim LastNote As Integer
    Dim Back As Integer
    Dim Err As String
    Dim E$

'**********************************************
'**********************************************

Main:
    ' Moved here from Sub cmdChords_Click
    ' This works but it not needed any more.
    ' Easier to use the "All" and "Ev song to ABC"
    ' buttons.
    With gSong(gSongCurr)
        If Left$(.T, 16) = "Write all chords" Then
            Call WriteAllChords
            Exit Sub
        End If
    End With

    GoSub Init
        'call InitProgressions
        'call InitErrorAllow
        'call InitVoiceRange ' once per song
        'GoSub Chords
            'call InitChordsForKey
            'call MainChordsForKey ' build chord tables
        'call SetNoteFlags
    GoSub MainLine
        'call Harmonize
        'call DisplayHarm
        'GoSub Err1
    Exit Sub

'**********************************************
'**********************************************

Init:
    txtPrint.Text = ""
    Call InitProgressions ' major, or minor natural
    Call InitErrorAllow ' gSongCurr - set gAllowErrors$
    Call InitVoiceRange ' gSongCurr - once per song
    gSongKey = -1 ' force new chords for the
    ' beginning of a new song in sub cmdChords_click.

    ' Reset for a new list of candidates in this song.
    gCandLast = 0

    ' Set for convenience:
    FirstNote = gSong(gSongCurr).FirstNote
    LastNote = gSong(gSongCurr).LastNote

    ' Set Globals:
        ' gNote(x).bBar, .bFirstChord,
        '   .bPrevChord, .bFinalChord
    Call SetNoteFlags(FirstNote, LastNote)
    Return
'********************************************

MainLine:
    gAfterX = 0
    Err = ""
    Call Harmonize(FirstNote, LastNote, _
        gAfterX, Err)
    Call DisplayHarm(gSongCurr)
    gbShowHarmony = True
    cmdShow.Caption = "&Show input"
    If Err <> "" Then
        GoSub Err1
    End If
    Return

'**********************************************

Err1:
    E$ = "cmdHarmony_Click: " _
        & "there is no 4-part solution " _
        & "to song=" & gSongCurr
    'txtPrint.Text = E$
    Call DisplayText(E$)
    Debug.Print E$
    Return
    'Stop

End Sub

Private Sub InitProgressions()
    ' called by sub cmdHarmony_Click
    ' Globals used:
    ' Globals set:

    ' TODO
    ' Add a check to exit right away if there
    ' is not need to change from major to minor
    ' or vice versa.
    Call InitProgMaj
    Call InitProgNatMin
    'call InitProgHarMin
    'call InitProgMelMin

    End Sub
Private Sub InitProgMaj()
'Private Sub InitProgMaj()
'Private Sub InitProgNatMin()
'Private Sub InitProgHarMin()
'Private Sub InitProgMelMin()

    ' called by sub InitProgressions
    ' Globals used:
    ' Globals set:
    '   gMajProgression(Row).bScaleDeg(Col)
    '   gMajProgression(Row).FromInversion(Col)
    ' These are used in sub ChordProgressions.

    Dim Col As Integer
    Dim Row As Integer
    Dim T(TONIC To LEADINGTONE) As Variant
    Dim MAJOR As Variant

    ' Given a scale degree category on top of the
    ' chord progression table, return
    ' the allowed harmonic chord progressions
    ' to it from the scale degrees on the Left$
    ' of the table.

    ' Major chord progession Ottman pg. 208
    ' vi-iii is uncommon except when followed
    ' by IV.
    ' Index 8 for ii 6, index 9 for vi-iii-IV

    ' While V does not resolve to IV in this style,
    ' it can resolve to IV6 (CounterPoint Syllabus
    ' by Doug Delaney p 13, Ottman p. 302).
    ' I assume any inversion for V-IV6, although
    ' probably root position V-IV6 was meant.

    ' The first chord must be in ROOTPOS in:
    ' iii-ii6, V-iii6, V-IV6, I-vi6
    ' I assume the first chord must be in ROOTPOS in:
    ' vi-iii-IV or VI-III-iv which can be replaced by:
    ' vi-iii-iio6 or VI-III-ii6.

    ' (1)Allow (N) vii o6 to IV retrograde and
    ' allow (N) V to IV for "Non Nobis, Domine".
    ' TODO try V to IV6 for "Non Nobis, Domine".
    ' TODO can vi6 go to iii-IV?
    ' Per Adrian Quince, COM tutor, any scale degree
    ' can go to itself, since there is no root
    ' movement.

' X means that Left-chord can go to Top-chord in the table.
' Y means that Left-chord must also be Root Position.
' N is X or Y by exception for some song.
' S means a scale degree goes to itself.

'        1,2, 3,  4, 5,6, 7,    8,  9,     10   11  12)
'        I ii iii IV V vi viio6 ii6 iii-IV iii6 IV6 vi6
'     I  X X   X  X  X X    X    X    X          X   Y
'    ii    S         X      X
'   iii        S  X  X X         Y               X
'    IV  X X      S  X      X    X
'     V  X        N  S X                    Y   (YX)
'    vi    X      X  X S         X    YX         X
' viio6  X        N         S                    N

' 1 means that Left-chord can go to Top-chord.
' 2 means that Left-chord must also be Root Position.
'T(0) = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12)
 T(1) = Array(0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 2)
 T(2) = Array(0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0)

 T(3) = Array(0, 0, 0, 1, 1, 1, 1, 0, 2, 0, 0, 1, 0)
 T(4) = Array(0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0)

'T(5) = Array(0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 2, 1, 0)
 T(5) = Array(0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 2, 2, 0)
'T(6) = Array(0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0)
 T(6) = Array(0, 0, 1, 0, 1, 1, 1, 0, 1, 2, 0, 1, 0)

'T(7) = Array(0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0)
 T(7) = Array(0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0)

    ' Remember that Boolean zero is logically false
    ' and anything else, especially -1, is True.
    For Row = TONIC To LEADINGTONE ' 1 to 7
        MAJOR = T(Row) ' array
        With gMajProgression(Row)
            For Col = 1 To PROGRESSIONMAX ' 12
                If MAJOR(Col) < 2 Then
                    ' 0 = False, -1 = True
                    .bScaleDeg(Col) = -MAJOR(Col)
                    .FromInversion(Col) = NOINVERSIONNEEDED
                ElseIf MAJOR(Col) = 2 Then
                    .bScaleDeg(Col) = True
                    .FromInversion(Col) = ROOTPOS
                End If
            Next Col
        End With
    Next Row
End Sub


Private Sub InitProgNatMin()
'Private Sub InitProgMaj()
'Private Sub InitProgNatMin()
'Private Sub InitProgHarMin()
'Private Sub InitProgMelMin()

    ' called by sub InitProgressions
    ' Globals used:
    ' Globals set:
    '   gMajProgression(J).bScaleDeg(I)
    '   gMajProgression(J).FromInversion(I)
    ' These are used in sub ChordProgressions.

    Dim I As Integer
    Dim J As Integer
    Dim T(TONIC To LEADINGTONE) As Variant
    Dim NatMin As Variant

    ' Given a scale degree category on top of the
    ' chord progression table, return
    ' the allowed harmonic chord progressions
    ' to it from the scale degrees on the Left$
    ' of the table.

    ' Natural minor chord progession Ottman pg. 208
    ' VII is built on b^7 (C minor: Bb D F).
    ' viio6 is built on #^7 (C minor: B D F).
    ' VI-III is uncommon except when followed
    ' by iv.
    ' Index 8 is unused, index 9 for VI-III-iv

    ' While V does not resolve to IV in this style,
    ' it can resolve to IV6 (CounterPoint Syllabus
    ' by Doug Delaney p 13).

    ' The first chord must be in ROOTPOS in:
    ' iii-iio6, V-iii6, V-IV6, I-vi6
    ' I assume the first chord must be in ROOTPOS in:
    ' vi-iii-IV or VI-III-iv which can be replaced by:
    ' vi-iii-iio6 or VI-III-ii6.

    ' Must allow (N) III-V or III-v (skip/elision twice)
    ' for Dies Irae (not allowed in minor mode
    ' in this style Ottman p 208)
    ' or must allow 7th chords by error/exception.

    ' Must allow (N) V-IV or v-iv for Dies Irae.
    ' TODO try V to IV6 for Dies Irae.
    ' TODO can VI6 go to III-iv?
    ' Per Adrian Quince, COM tutor, any scale degree
    ' can go to itself, since there is no root
    ' movement.

   ' Original natural matrix:
' X means that Left$-chord can go to top-chord.
' Y means that Left$-chord must also be root position.
' N is X or Y by exception for some song.
' S means a scale degree goes to itself.

'      1,2,   3,  4, 5,6, 7,  8,  9,     10   11  12
'      i iio6 III iv v VI VII (0) III-iv iii6 iv6 VI6
'   i  X  X    X  X  X X   X        X          X   Y
'iio6     S          X     N
' III     Y    S  X  N X                       X
'  iv  X  X       S  X     X
'  *v  X          N  S X                  Y    YX
'  VI     X       X  X S            YX         X
' VII  X       X           S        X

' 1 means that Left-chord can go to top-chord.
' 2 means that Left-chord must also be root position.

'T(0) = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12)
 T(1) = Array(0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 2)

'T(2) = Array(0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0)
 T(2) = Array(0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0)

'T(3) = Array(0, 0, 2, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0)
 T(3) = Array(0, 0, 2, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0)
 T(4) = Array(0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0)

'T(5) = Array(0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 2, 2, 0)
 T(5) = Array(0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 2, 2, 0)
 T(6) = Array(0, 0, 1, 0, 1, 1, 1, 0, 0, 2, 0, 1, 0)

'T(7) = Array(0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0)
 T(7) = Array(0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0)

    ' Remember that Boolean zero is logically false
    ' and anything else, especially -1, is True.
    For J = TONIC To LEADINGTONE ' 1 to 7
        NatMin = T(J) ' array
        With gMinNatProg(J)
            For I = 1 To PROGRESSIONMAX ' 10
                If NatMin(I) < 2 Then
                    ' 0 = False, -1 = True
                    .bScaleDeg(I) = -NatMin(I)
                    .FromInversion(I) = NOINVERSIONNEEDED
                ElseIf NatMin(I) = 2 Then
                    .bScaleDeg(I) = True
                    .FromInversion(I) = ROOTPOS
                End If
            Next I
        End With
    Next J

' *******************************************
' NOT USED YET:

   ' MinProg = Array("", "", "ii o-V ii-V ii-vii o", _
   '    "III-ii o III-iv III-v III-VI", _
   '    "iv-i iv-ii o iv-V iv-VII IV-V IV-vii o", _
   '    "V-i V-iii V-IV V-VI v-i v-III v-iv v-VI", _
   '    "VI-ii o VI-iv VI-V VI-III", _
   '    "vii o-i VII-III")
        ' "III-ii o III-iv III-VI", _
        ' "V-i V-VI v-i v-VI", _
        ' "VI-ii o VI-iv VI-V VI-III-iv",

    ' Allow (N) III to V or v skip/elision twice
    ' for Dies Irae (in major mode progression)
    ' Allow V-IV or v-iv for Dies Irae (test).

    ' TODO
    ' Matrix allowing any minor to minor
    ' progression.  The program should raise
    ' or lower any necesary 3rds, etc.
    'T(0) = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
    ' X:     i iio6 III iv V VI VII   (-) III-iv
    '     i  X  X    X  X  X X   X         X
    '  iio6                X
    '   III     X       X    X
    '    iv  X  X          X     X
    '     V  X               X
    '    VI     X       X  X               X
    '   VII          X

    ' Y:                IV V VI viio6
    '     i  X  X    X  X  X X   X         X
    '   ii*                Y     Y
    '   IV*                Y     Y
    '    v*                  Y
    'viio6*  X

    ' Z:                   v
    '     i  X  X    X  X  X X   X         X

 ' 'T(0) = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
 '  T(1) = Array(0, 1, 1, 1, 1, 1, 1, 1, 0, 1)
 '  T(2) = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0)
 ' 'T(3) = Array(0, 0, 1, 0, 1, 0, 1, 0, 0, 0)
 '  T(3) = Array(0, 0, 1, 0, 1, 1, 1, 0, 0, 0)
 '  T(4) = Array(0, 1, 1, 0, 0, 1, 0, 1, 0, 0)
 ' 'T(5) = Array(0, 1, 0, 0, 0, 0, 1, 0, 0, 0)
 '  T(5) = Array(0, 1, 0, 0, 1, 0, 1, 0, 0, 0)
 '  T(6) = Array(0, 0, 1, 0, 1, 1, 0, 0, 0, 1)
 '  T(7) = Array(0, 1, 0, 1, 0, 0, 0, 0, 0, 1)

End Sub

Private Sub InitProgHarMin()
'Private Sub InitProgMaj()
'Private Sub InitProgNatMin()
'Private Sub InitProgHarMin()
'Private Sub InitProgMelMin()
    ' called by sub InitProgressions
    ' Globals used: in sub ChordProgressions
    ' Globals set:
    '   gMajProgression(J).bScaleDeg(I)
    '   gMajProgression(J).FromInversion(I)

    ' TODO
    ' Add a check to exit right away if there
    ' is not need to change from major to minor
    ' or vice versa.

    Dim I As Integer
    Dim J As Integer
    Dim T(TONIC To LEADINGTONE) As Variant
    Dim MAJOR As Variant
    Dim NatMin As Variant
    'Dim Maj1, Maj2, Maj3, Maj4, Maj5, Maj6, Maj7
    'Dim Nat1, Nat2, Nat3, Nat4, Nat5, Nat6, Nat7
    Dim Har1, Har2, Har3, Har4, Har5, Har6, Har7 ' variants
    Dim Mel1, Mel2, Mel3, Mel4, Mel5, Mel6, Mel7 ' variants

    ' Given a scale degree category on top of the
    ' chord progression table, return
    ' the allowed harmonic chord progressions
    ' to it from the scale degrees on the Left$
    ' of the table.

    ' Major chord progession Ottman pg. 208
    ' vi-iii is uncommon except when followed
    ' by IV.
    ' Index 8 for ii 6, index 9 for vi-iii-IV

    ' While V does not resolve to IV in this style,
    ' it can resolve to IV6 (CounterPoint Syllabus
    ' by Doug Delaney p 13).

    ' The first chord must be in ROOTPOS in:
    ' iii-ii6, V-iii6, V-IV6, I-vi6
    ' I assume the first chord must be in ROOTPOS in:
    ' vi-iii-IV or VI-III-iv which can be replaced by:
    ' vi-iii-iio6 or VI-III-ii6.

    ' (1)Allow (N) vii o6 to IV retrograde and
    ' allow (N) V to IV for "Non Nobis, Domine".
    ' TODO try V to IV6 for "Non Nobis, Domine".

' X means that Left$-chord can go to top-chord.
' Y means that Left$-chord must also be 1st inversion.
'        1,2, 3,  4, 5,6, 7,    8,  9,     10   11  12)
'        I ii iii IV V vi viio6 ii6 iii-IV iii6 IV6 vi6
'     I  X X   X  X  X X    X    X    X          X   Y
'    ii              X      X
'   iii           X  X X         Y               X
'    IV  X X         X      X
'     V  X        N    X                    Y    Y
'    vi    X      X  X                Y          X
' viio6  X        N                              N

' 1 means that Left$-chord can go to top-chord.
' 2 means that Left$-chord must also be 1st inversion.
'T(0) = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12)
 T(1) = Array(0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 2)
 T(2) = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0)
 T(3) = Array(0, 0, 0, 0, 1, 1, 1, 0, 2, 0, 0, 1, 0)
 T(4) = Array(0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0)
 T(5) = Array(0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 2, 2, 0)
 T(6) = Array(0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0)

'T(7) = Array(0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0)
 T(7) = Array(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

    ' Note that "-1" is logically "True"
    For J = TONIC To LEADINGTONE ' 1 to 7
        MAJOR = T(J) ' array
        With gMinHarmProg(J)
            For I = 1 To PROGRESSIONMAX ' 10
                If MAJOR(I) < 2 Then
                    .bScaleDeg(I) = -MAJOR(I)
                    .FromInversion(I) = NOINVERSIONNEEDED
                ElseIf .FromInversion(I) = 2 Then
                    .bScaleDeg(I) = True
                    .FromInversion(I) = ROOTPOS
                End If
            Next I
        End With
    Next J

    ' Natural minor chord progession Ottman pg. 208
    ' VII is built on b^7 (C minor: Bb D F).
    ' viio6 is built on #^7 (C minor: B D F).
    ' VI-III is uncommon except when followed
    ' by iv.
    ' Index 8 is unused, index 9 for VI-III-iv

    ' The first chord must be in ROOTPOS in:
    ' iii-iio6, V-iii6, V-IV6, I-vi6
    ' I assume the first chord must be in ROOTPOS in:
    ' vi-iii-IV or VI-III-iv which can be replaced by:
    ' vi-iii-iio6 or VI-III-ii6.

    ' May allow (N) III-V or III-v (skip/elision twice)
    ' or V-III or v-iii
    ' for Dies Irae (in major mode progression).

    ' Must allow (N) V-IV or v-iv for Dies Irae.
    ' TODO try V to IV6 for Dies Irae.
    ' TODO can VI6 go to III-iv?

   ' Original natural matrix:
' X means that Left$-chord can go to top-chord.
' Y means that Left$-chord must also be 1st inversion.
' N is X or Y by exception for some song.

'      1,2,   3,  4, 5,6, 7,  8,  9,     10   11  12
'      i iio6 III iv v VI VII (0) III-iv iii6 iv6 VI6
'   i  X  X    X  X  X X   X        X          X   Y
'iio6                X     N
' III     Y       X    X                       X
'  iv  X  X          X     X
'  *v  X          N    X                  Y    Y
'  VI     X       X  X              Y          X
' VII  X       X                    X
' 1 means that Left$-chord can go to top-chord.
' 2 means that Left$-chord must also be 1st inversion.

'T(0) = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12)
 T(1) = Array(0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 2)

'T(2) = Array(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0)
 T(2) = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0)

'T(3) = Array(0, 0, 2, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0)
 T(3) = Array(0, 0, 2, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0)
 T(4) = Array(0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0)

'T(5) = Array(0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 2, 2, 0)
 T(5) = Array(0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 2, 2, 0)
 T(6) = Array(0, 0, 1, 0, 1, 1, 0, 0, 0, 2, 0, 1, 0)

'T(7) = Array(0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0)
 T(7) = Array(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0)

   ' MinProg = Array("", "", "ii o-V ii-V ii-vii o", _
   '    "III-ii o III-iv III-v III-VI", _
   '    "iv-i iv-ii o iv-V iv-VII IV-V IV-vii o", _
   '    "V-i V-iii V-IV V-VI v-i v-III v-iv v-VI", _
   '    "VI-ii o VI-iv VI-V VI-III", _
   '    "vii o-i VII-III")
        ' "III-ii o III-iv III-VI", _
        ' "V-i V-VI v-i v-VI", _
        ' "VI-ii o VI-iv VI-V VI-III-iv",

    ' Allow (N) III to V or v skip/elision twice
    ' for Dies Irae (in major mode progression)
    ' Allow V-IV or v-iv for Dies Irae (test).

    ' TODO
    ' Matrix allowing any minor to minor
    ' progression.  The program should raise
    ' or lower any necesary 3rds, etc.
    'T(0) = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
    ' X:     i iio6 III iv V VI VII   (-) III-iv
    '     i  X  X    X  X  X X   X         X
    '  iio6                X
    '   III     X       X    X
    '    iv  X  X          X     X
    '     V  X               X
    '    VI     X       X  X               X
    '   VII          X

    ' Y:                IV V VI viio6
    '     i  X  X    X  X  X X   X         X
    '   ii*                Y     Y
    '   IV*                Y     Y
    '    v*                  Y
    'viio6*  X

    ' Z:                   v
    '     i  X  X    X  X  X X   X         X

 ' 'T(0) = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
 '  T(1) = Array(0, 1, 1, 1, 1, 1, 1, 1, 0, 1)
 '  T(2) = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0)
 ' 'T(3) = Array(0, 0, 1, 0, 1, 0, 1, 0, 0, 0)
 '  T(3) = Array(0, 0, 1, 0, 1, 1, 1, 0, 0, 0)
 '  T(4) = Array(0, 1, 1, 0, 0, 1, 0, 1, 0, 0)
 ' 'T(5) = Array(0, 1, 0, 0, 0, 0, 1, 0, 0, 0)
 '  T(5) = Array(0, 1, 0, 0, 1, 0, 1, 0, 0, 0)
 '  T(6) = Array(0, 0, 1, 0, 1, 1, 0, 0, 0, 1)
 '  T(7) = Array(0, 1, 0, 1, 0, 0, 0, 0, 0, 1)

    ' Note that "-1" is logically "True"
    For J = TONIC To LEADINGTONE ' 1 to 7
        NatMin = T(J) ' array
        With gMajProgression(J)
            For I = 1 To PROGRESSIONMAX ' 10
                If NatMin(I) < 2 Then
                    .bScaleDeg(I) = -NatMin(I)
                    .FromInversion(I) = NOINVERSIONNEEDED
                ElseIf NatMin(I) = 2 Then
                    .bScaleDeg(I) = True
                    .FromInversion(I) = ROOTPOS
                End If
            Next I
        End With
    Next J


    ' Harmonic minor chord progession Ottman pg. 208
    ' Per Ottman,
    ' ii and IV in minor are used with ascending
    ' #^6.  v in minor is used with a descending ^7.
    ' By deduction, I also think that
    ' viio6 in minor is used along with ii with
    ' ascending #^6, and VII in minor is used
    ' along with iv with a descending ^7.
    ' The triad on #^6 is seldon seen (C minor:
    ' A C Eb instead of b^6 Ab C Eb) Ottman p 208.
    ' Index 8 is unused, index 9 for VI-III-iv
    '        i iio6 III iv V VI viio6 (-) III-iv iv6
    '     i  X  X    X  X  X X    X         X     X
    '    II                X      X
    'III(+)     X       X    X                    X
    '   *IV                X      X
    '     V  X                X                   X
    '    VI     X       X  X                X     X
    '*viio6  X
   'Har0 = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    Har1 = Array(0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1)
    Har2 = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0)
    Har3 = Array(0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1)
    Har4 = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0)
    Har5 = Array(0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1)
    Har6 = Array(0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1)
    Har7 = Array(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0)
    For I = 1 To PROGRESSIONMAX ' 10
        gMinHarmProg(1).bScaleDeg(I) = -Har1(I)
        gMinHarmProg(2).bScaleDeg(I) = -Har2(I)
        gMinHarmProg(3).bScaleDeg(I) = -Har3(I)
        gMinHarmProg(4).bScaleDeg(I) = -Har4(I)
        gMinHarmProg(5).bScaleDeg(I) = -Har5(I)
        gMinHarmProg(6).bScaleDeg(I) = -Har6(I)
        gMinHarmProg(7).bScaleDeg(I) = -Har7(I)
    Next I


    ' Melodic ascending minor chord progession
    ' Ottman pg. 208  ??
    ' Index 8 is unused, index 9 for VI-III-iv
    '        i iio6 III iv V VI viio6 (-) III-iv iv6
    '     i  X  X    X  X  X X    X         X     X
    '   *II                X      X
    'III(+)     X       X    X                    X
    '   *IV                X      X
    '     V  X               X                    X
    '    VI     X       X  X                X     X
    '*viio6  X
   'Mel0 = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    Mel1 = Array(0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1)
    Mel2 = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0)
    Mel3 = Array(0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1)
    Mel4 = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0)
    Mel5 = Array(0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1)
    Mel6 = Array(0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1)
    Mel7 = Array(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0)
    For I = 1 To PROGRESSIONMAX ' 10
        gMinMelUpProg(1).bScaleDeg(I) = -Mel1(I)
        gMinMelUpProg(2).bScaleDeg(I) = -Mel2(I)
        gMinMelUpProg(3).bScaleDeg(I) = -Mel3(I)
        gMinMelUpProg(4).bScaleDeg(I) = -Mel4(I)
        gMinMelUpProg(5).bScaleDeg(I) = -Mel5(I)
        gMinMelUpProg(6).bScaleDeg(I) = -Mel6(I)
        gMinMelUpProg(7).bScaleDeg(I) = -Mel7(I)
    Next I

End Sub



Private Sub InitProgMelMin()
'Private Sub InitProgMaj()
'Private Sub InitProgNatMin()
'Private Sub InitProgHarMin()
'Private Sub InitProgMelMin()
    ' called by sub InitProgressions
    ' Globals used: in sub ChordProgressions
    ' Globals set:
    '   gMajProgression(J).bScaleDeg(I)
    '   gMajProgression(J).FromInversion(I)

    ' TODO
    ' Add a check to exit right away if there
    ' is not need to change from major to minor
    ' or vice versa.

    Dim I As Integer
    Dim J As Integer
    Dim T(TONIC To LEADINGTONE) As Variant
    Dim MAJOR As Variant
    Dim NatMin As Variant
    'Dim Maj1, Maj2, Maj3, Maj4, Maj5, Maj6, Maj7
    'Dim Nat1, Nat2, Nat3, Nat4, Nat5, Nat6, Nat7
    Dim Har1, Har2, Har3, Har4, Har5, Har6, Har7 ' variants
    Dim Mel1, Mel2, Mel3, Mel4, Mel5, Mel6, Mel7 ' variants

    ' Given a scale degree category on top of the
    ' chord progression table, return
    ' the allowed harmonic chord progressions
    ' to it from the scale degrees on the Left$
    ' of the table.

    ' Major chord progession Ottman pg. 208
    ' vi-iii is uncommon except when followed
    ' by IV.
    ' Index 8 for ii 6, index 9 for vi-iii-IV

    ' While V does not resolve to IV in this style,
    ' it can resolve to IV6 (CounterPoint Syllabus
    ' by Doug Delaney p 13).

    ' The first chord must be in ROOTPOS in:
    ' iii-ii6, V-iii6, V-IV6, I-vi6
    ' I assume the first chord must be in ROOTPOS in:
    ' vi-iii-IV or VI-III-iv which can be replaced by:
    ' vi-iii-iio6 or VI-III-ii6.

    ' (1)Allow (N) vii o6 to IV retrograde and
    ' allow (N) V to IV for "Non Nobis, Domine".
    ' TODO try V to IV6 for "Non Nobis, Domine".

' X means that Left$-chord can go to top-chord.
' Y means that Left$-chord must also be 1st inversion.
'        1,2, 3,  4, 5,6, 7,    8,  9,     10   11  12)
'        I ii iii IV V vi viio6 ii6 iii-IV iii6 IV6 vi6
'     I  X X   X  X  X X    X    X    X          X   Y
'    ii              X      X
'   iii           X  X X         Y               X
'    IV  X X         X      X
'     V  X        N    X                    Y    Y
'    vi    X      X  X                Y          X
' viio6  X        N                              N

' 1 means that Left$-chord can go to top-chord.
' 2 means that Left$-chord must also be 1st inversion.
'T(0) = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12)
 T(1) = Array(0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 2)
 T(2) = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0)
 T(3) = Array(0, 0, 0, 0, 1, 1, 1, 0, 2, 0, 0, 1, 0)
 T(4) = Array(0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0)
 T(5) = Array(0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 2, 2, 0)
 T(6) = Array(0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0)

'T(7) = Array(0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0)
 T(7) = Array(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

    ' Note that "-1" is logically "True"
    For J = TONIC To LEADINGTONE ' 1 to 7
        MAJOR = T(J) ' array
        With gMinMelUpProg(J)
            For I = 1 To PROGRESSIONMAX ' 10
                If MAJOR(I) < 2 Then
                    .bScaleDeg(I) = -MAJOR(I)
                    .FromInversion(I) = NOINVERSIONNEEDED
                ElseIf .FromInversion(I) = 2 Then
                    .bScaleDeg(I) = True
                    .FromInversion(I) = ROOTPOS
                End If
            Next I
        End With
    Next J

    ' Natural minor chord progession Ottman pg. 208
    ' VII is built on b^7 (C minor: Bb D F).
    ' viio6 is built on #^7 (C minor: B D F).
    ' VI-III is uncommon except when followed
    ' by iv.
    ' Index 8 is unused, index 9 for VI-III-iv

    ' The first chord must be in ROOTPOS in:
    ' iii-iio6, V-iii6, V-IV6, I-vi6
    ' I assume the first chord must be in ROOTPOS in:
    ' vi-iii-IV or VI-III-iv which can be replaced by:
    ' vi-iii-iio6 or VI-III-ii6.

    ' May allow (N) III-V or III-v (skip/elision twice)
    ' or V-III or v-iii
    ' for Dies Irae (in major mode progression).

    ' Must allow (N) V-IV or v-iv for Dies Irae.
    ' TODO try V to IV6 for Dies Irae.
    ' TODO can VI6 go to III-iv?

   ' Original natural matrix:
' X means that Left$-chord can go to top-chord.
' Y means that Left$-chord must also be 1st inversion.
' N is X or Y by exception for some song.

'      1,2,   3,  4, 5,6, 7,  8,  9,     10   11  12
'      i iio6 III iv v VI VII (0) III-iv iii6 iv6 VI6
'   i  X  X    X  X  X X   X        X          X   Y
'iio6                X     N
' III     Y       X    X                       X
'  iv  X  X          X     X
'  *v  X          N    X                  Y    Y
'  VI     X       X  X              Y          X
' VII  X       X                    X
' 1 means that Left$-chord can go to top-chord.
' 2 means that Left$-chord must also be 1st inversion.

'T(0) = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12)
 T(1) = Array(0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 2)

'T(2) = Array(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0)
 T(2) = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0)

'T(3) = Array(0, 0, 2, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0)
 T(3) = Array(0, 0, 2, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0)
 T(4) = Array(0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0)

'T(5) = Array(0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 2, 2, 0)
 T(5) = Array(0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 2, 2, 0)
 T(6) = Array(0, 0, 1, 0, 1, 1, 0, 0, 0, 2, 0, 1, 0)

'T(7) = Array(0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0)
 T(7) = Array(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0)

   ' MinProg = Array("", "", "ii o-V ii-V ii-vii o", _
   '    "III-ii o III-iv III-v III-VI", _
   '    "iv-i iv-ii o iv-V iv-VII IV-V IV-vii o", _
   '    "V-i V-iii V-IV V-VI v-i v-III v-iv v-VI", _
   '    "VI-ii o VI-iv VI-V VI-III", _
   '    "vii o-i VII-III")
        ' "III-ii o III-iv III-VI", _
        ' "V-i V-VI v-i v-VI", _
        ' "VI-ii o VI-iv VI-V VI-III-iv",

    ' Allow (N) III to V or v skip/elision twice
    ' for Dies Irae (in major mode progression)
    ' Allow V-IV or v-iv for Dies Irae (test).

    ' TODO
    ' Matrix allowing any minor to minor
    ' progression.  The program should raise
    ' or lower any necesary 3rds, etc.
    'T(0) = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
    ' X:     i iio6 III iv V VI VII   (-) III-iv
    '     i  X  X    X  X  X X   X         X
    '  iio6                X
    '   III     X       X    X
    '    iv  X  X          X     X
    '     V  X               X
    '    VI     X       X  X               X
    '   VII          X

    ' Y:                IV V VI viio6
    '     i  X  X    X  X  X X   X         X
    '   ii*                Y     Y
    '   IV*                Y     Y
    '    v*                  Y
    'viio6*  X

    ' Z:                   v
    '     i  X  X    X  X  X X   X         X

 ' 'T(0) = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
 '  T(1) = Array(0, 1, 1, 1, 1, 1, 1, 1, 0, 1)
 '  T(2) = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0)
 ' 'T(3) = Array(0, 0, 1, 0, 1, 0, 1, 0, 0, 0)
 '  T(3) = Array(0, 0, 1, 0, 1, 1, 1, 0, 0, 0)
 '  T(4) = Array(0, 1, 1, 0, 0, 1, 0, 1, 0, 0)
 ' 'T(5) = Array(0, 1, 0, 0, 0, 0, 1, 0, 0, 0)
 '  T(5) = Array(0, 1, 0, 0, 1, 0, 1, 0, 0, 0)
 '  T(6) = Array(0, 0, 1, 0, 1, 1, 0, 0, 0, 1)
 '  T(7) = Array(0, 1, 0, 1, 0, 0, 0, 0, 0, 1)

    ' Note that "-1" is logically "True"
    For J = TONIC To LEADINGTONE ' 1 to 7
        NatMin = T(J) ' array
        With gMajProgression(J)
            For I = 1 To PROGRESSIONMAX ' 10
                If NatMin(I) < 2 Then
                    .bScaleDeg(I) = -NatMin(I)
                    .FromInversion(I) = NOINVERSIONNEEDED
                ElseIf .FromInversion(I) = 2 Then
                    .bScaleDeg(I) = True
                    .FromInversion(I) = ROOTPOS
                End If
            Next I
        End With
    Next J


    ' Harmonic minor chord progession Ottman pg. 208
    ' Per Ottman,
    ' ii and IV in minor are used with ascending
    ' #^6.  v in minor is used with a descending ^7.
    ' By deduction, I also think that
    ' viio6 in minor is used along with ii with
    ' ascending #^6, and VII in minor is used
    ' along with iv with a descending ^7.
    ' The triad on #^6 is seldon seen (C minor:
    ' A C Eb instead of b^6 Ab C Eb) Ottman p 208.
    ' Index 8 is unused, index 9 for VI-III-iv
    '        i iio6 III iv V VI viio6 (-) III-iv iv6
    '     i  X  X    X  X  X X    X         X     X
    '    II                X      X
    'III(+)     X       X    X                    X
    '   *IV                X      X
    '     V  X                X                   X
    '    VI     X       X  X                X     X
    '*viio6  X
   'Har0 = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    Har1 = Array(0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1)
    Har2 = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0)
    Har3 = Array(0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1)
    Har4 = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0)
    Har5 = Array(0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1)
    Har6 = Array(0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1)
    Har7 = Array(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0)
    For I = 1 To PROGRESSIONMAX ' 10
        gMinHarmProg(1).bScaleDeg(I) = -Har1(I)
        gMinHarmProg(2).bScaleDeg(I) = -Har2(I)
        gMinHarmProg(3).bScaleDeg(I) = -Har3(I)
        gMinHarmProg(4).bScaleDeg(I) = -Har4(I)
        gMinHarmProg(5).bScaleDeg(I) = -Har5(I)
        gMinHarmProg(6).bScaleDeg(I) = -Har6(I)
        gMinHarmProg(7).bScaleDeg(I) = -Har7(I)
    Next I


    ' Melodic ascending minor chord progession
    ' Ottman pg. 208  ??
    ' Index 8 is unused, index 9 for VI-III-iv
    '        i iio6 III iv V VI viio6 (-) III-iv iv6
    '     i  X  X    X  X  X X    X         X     X
    '   *II                X      X
    'III(+)     X       X    X                    X
    '   *IV                X      X
    '     V  X               X                    X
    '    VI     X       X  X                X     X
    '*viio6  X
   'Mel0 = Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    Mel1 = Array(0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1)
    Mel2 = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0)
    Mel3 = Array(0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1)
    Mel4 = Array(0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0)
    Mel5 = Array(0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1)
    Mel6 = Array(0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1)
    Mel7 = Array(0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0)
    For I = 1 To PROGRESSIONMAX ' 10
        gMinMelUpProg(1).bScaleDeg(I) = -Mel1(I)
        gMinMelUpProg(2).bScaleDeg(I) = -Mel2(I)
        gMinMelUpProg(3).bScaleDeg(I) = -Mel3(I)
        gMinMelUpProg(4).bScaleDeg(I) = -Mel4(I)
        gMinMelUpProg(5).bScaleDeg(I) = -Mel5(I)
        gMinMelUpProg(6).bScaleDeg(I) = -Mel6(I)
        gMinMelUpProg(7).bScaleDeg(I) = -Mel7(I)
    Next I

End Sub

Private Sub InitErrorAllow()
    ' called by sub cmdHarmony_Click
    ' calls

    ' Globals used: gFieldLast, gField(I)
    ' Globals set: gAllowErrors$

    ' Store the options input on the OP: line.
    ' The ALLOW option specifies the error types
    ' allowed in this song. E.g.
    ' OP: ALLOW P5 sim4
    ' OP: ERROR prog
    ' OP: ERROR NONE
    ' where each field is separated by tab.
    ' Same for ERROR.
    ' The last line read,  ALLOW or ERROR,
    ' takes effect and nullifies any previous.

    ' severity 1: (worst)
    ' "P5","P8"
    ' severity 2:
    ' ,"Hid8","Hid5","Prog63","ProgNv","NotFin"
    ' severity 3:
    ' ,"Sim4","BxT","TxA","AxS"
    ' severity 4:
    ' ,"Invr","Not7th" (requested in input by user)
    ' ,"User" (requested on screen by user)

    ' pErr=
    ' "P5","P8","Sim4","BxT","TxA","AxS",
    ' "Hid8","Hid5",
    ' ( "~#3","~b3","NoSol" )

    ' Err=
    ' ""NotFin","ProgNv",
    ' "Invr","Prog63",
    ' ( "NoMch",Not7th", "2Inv","NoSol" )

    ' .Error =
    ' gCand(gCandLast).Error = Err

    ' gCand(.BestCand).Error = "No sol"
    ' gCand(I).Error2 = "Final"
    '   ( 'gCand(I).Error = "Var=0" )
    ' If gCand(I1).Error = "~b3"
    ' gCand(SaveBestCand).Error = "Soln" or "^Soln"

    ' Err2 =
    ' If Err2 = "Final" Then

    ' For every bit set in xErrAllowed,
    ' concatenate the corresponding error name
    ' from gErrName following its index 1 - 31
    ' into the string gAllowErrors$, which
    ' will be searched by sub fcnErrAllow.

    Dim I As Integer
    Dim xErrAllowed As Long
    Dim xErrAllowedPass1 As Long

    xErrAllowed = _
        gSong(gSongCurr).xErrorAllowed
    gAllowErrors$ = ""
    For I = 1 To ERRNAMEMAX
        If xErrAllowed And 2 ^ (I - 1) Then
            gAllowErrors$ = gAllowErrors$ & _
                Str$(I) & gErrName(I)
        End If
    Next I

    xErrAllowedPass1 = _
        gSong(gSongCurr).xErrorAllowedPass1
    gAllowErrorsPass1$ = " "
    For I = 1 To ERRNAMEMAX
        If xErrAllowedPass1 And 2 ^ (I - 1) Then
            gAllowErrorsPass1$ = gAllowErrorsPass1$ & _
                gErrName(I) & " "
        End If
    Next I
End Sub


Private Function fcnErrAllow(pNoteX As Integer, _
                            pText As String) _
                            As String
    ' called by sub ChordProgressions
    ' called by sub fcnbCandChord
    ' called by sub fcnbParallel

    ' Globals used: gFieldLast, gField(I)
    ' Globals set:
    '   gxErrorAllowed(J)=True
    '   gSong(gSongLast).bTriadAllowed(I) =

    ' Store the options input on the OP: line.
    ' The ALLOW option specifies the Roman
    ' numerals allowed as triad numbers in this
    ' son.  E.g.
    ' OP: ALLOW I V
    ' OP: ERROR I V
    ' OP: ERROR NONE
    ' where each field is separated by tab.
    ' Same for ERROR.

    ' severity 1: (worst)
    ' "P5","P8"
    ' severity 2:
    ' ,"Hid8","Hid5","Prog63","ProgNv",
    ' ,"PrgAny","PrgBas","PrgExt","Prg6th",Prg7th",
    ' "NotFin"
    ' severity 3:
    ' ,"Sim4","BxT","TxA","AxS"
    ' severity 4:
    ' ,"Invr","Not7th" (requested in input by user)
    ' ,"User" (requested on screen by user)

    ' pErr=
    ' "P5","P8","Sim4","BxT","TxA","AxS",
    ' "Hid8","Hid5",
    ' ( "~#3","~b3","NoSol" )

    ' Err=
    ' ""NotFin","ProgNv",
    ' "Invr","Prog63",
    ' ( "NoMch",Not7th", "2Inv","NoSol" )

    ' .Error =
    ' gCand(gCandLast).Error = Err

    ' gCand(.BestCand).Error = "No sol"
    ' gCand(I).Error2 = "Final"
    '   ( 'gCand(I).Error = "Var=0" )
    ' If gCand(I1).Error = "~b3"
    ' gCand(SaveBestCand).Error = "Soln" or "^Soln"

    ' Err2 =
    ' If Err2 = "Final" Then

    ' For every bit set in gxErrorAllowed,
    ' concatenate the corresponding error name
    ' from gErrName following its index 1 - 31
    ' into the string gAllowErrors$, which
    ' will be searched by sub fcnErrAllow.

    Dim Err As String
    Dim A$
    Dim E$
    Dim I As Integer
    Dim Power As Integer
    Dim bDebugErrAllow As Boolean

   'If gbNewCand Then ' reset in fcnbCandChord
   '    gbNewCand = False
   '    gLowestErrNum = 99
   '    gNumCandErrs = 0
   '    gSingleAllowedErrNum = 0
   '    gLowestAllowedErrNum = 99
   '    gNumAllowedCandErrs = 0
   'End If
    ' fcnbCandChord and fcnbParallel scan for all
    ' possible errors out of the 31 (so far).

    ' Only one Allowed Error in the list
    '   OP: ALLOW errname errname2
    ' is accepted for a candidate at present.
    ' It is displayed with a leading paren in
    '   gCand(.BestCand).Error="(MyErr".

    ' After the first Allowed Error is found,
    ' if any others in the allowed list are found,
    ' the error that is lowest in the list
    ' (e.g. P8 is first) is displayed
    ' with a leading bracket in
    '   gCand(.BestCand).Error="[MyErr".

    ' If errors that are not allowed for this
    ' song are found, the error that is lowest in
    ' the list (e.g. P8 is first) is displayed in
    '   gCand(.BestCand).Error="MyErr".

    ' Find the string Err in the string of all
    ' errornames, and get the two digit number
    ' in front of it.  Turn on that error bit
    ' for this candidate chord.
    ' Note - the error bits are not used
    ' individually at present.
    
    'bDebugErrAllow = True
    'bDebugErrAllow = False

    Err = UCase$(pText)
    ' TODO this is a cheap way to disable P4INT
    'If Err = "P4INT" Then
    'If Err = "SIM4" Then
    '    I = I
        ' (1) this works with (ptext displayed:
        'gNote(pNoteX).bMustAllowError = True
        
        ' (2) this works without (ptext displayed:
        'pText = "(" & pText
        'fcnErrAllow = "(" & pText
        'Exit Function
    'End If
    
    ' The option OP: ALLOW PASS1 sim4
    ' allows selecting the allowed error
    ' chord (SIM4 on Pass1 instead of Pass4.
    I = InStr(gAllowErrorsPass1$, Err)
    If I > 0 Then
        gNote(pNoteX).bMustAllowError = True
        I = I
    End If
    
    
    I = InStr(gErrorNames$, Err)
    If I > 0 Then
        A$ = Mid$(gErrorNames$, I - 2, 2)
        Power = Val(A$)
        gxCandErrorBits = gxCandErrorBits _
            Or 2 ^ (Power - 1)
    Else
        GoSub Err1
    End If

    ' If this is the fourth pass (the second
    ' back-tracking pass) in sub Harmonize,
    ' or if this note was found to need an
    ' error exception,
    '   function fcnbCandChord
    '   gNote(pNoteX).bMustAllowError
    ' then check to see if
    ' this error item is in the allowed list.
    ' If so, pass back a leading paren, which
    ' will be stored in gCand(.BestCand).Error
    ' and used in sub ChoosePrevChord.
    If gbErrorFourthPass _
    Or gNote(pNoteX).bMustAllowError Then
        If InStr(gAllowErrors$, Err) > 0 Then
            If gSingleAllowedErrNum = 0 Then
                gNumAllowedCandErrs = 1
                gSingleAllowedErrNum = Power
                gLowestAllowedErrNum = Power
                pText = "(" & pText
                GoSub Err2 ' TODO debug
            Else
                gNumAllowedCandErrs = _
                    gNumAllowedCandErrs + 1
                If Power < gLowestAllowedErrNum Then
                    gLowestAllowedErrNum = Power
                End If
                pText = "(<" & pText
                GoSub Err3 ' TODO debug
            End If
        Else
            gNumCandErrs = gNumCandErrs + 1
            If Power < gLowestErrNum Then
                gLowestErrNum = Power
            End If
        End If
    Else
        gNumCandErrs = gNumCandErrs + 1
        If Power < gLowestErrNum Then
            gLowestErrNum = Power
        End If
    End If
    fcnErrAllow = pText: 'Debug.Print "EA:"; pText
    Exit Function

Err1:
    If Not bDebugErrAllow Then
        'Return
    End If
    E$ = "fcnErrAllow: error name=" & pText & _
        " was not found in gErrorNames$ list"
    Call DisplayText(E$)
    Debug.Print E$
    Stop
    Return

Err2:
    If Not bDebugErrAllow Then
        Return
    End If
    E$ = "fcnErrAllow: at gNoteX=" & gNoteX _
        & ", single allowed error name=" _
        & pText _
        & ", power=" & Power
    Call DisplayText(E$)
    Debug.Print E$
    'Stop
    I = I ' testing
    Return

Err3:
    If Not bDebugErrAllow Then
        Return
    End If
    E$ = "fcnErrAllow: at gNoteX=" & gNoteX _
        & ", lowest and newest error names=" _
        & gErrName(gLowestAllowedErrNum) _
        & " " & gLowestAllowedErrNum _
        & ", " & pText _
        & ", power=" & Power
    Call DisplayText(E$)
    Debug.Print E$
    'Stop
    I = I ' testing
    Return

End Function

Private Sub WriteDblTypes()
    ' called by sub WriteAllChords
    ' Globals used: gDblNames()=, gDblDescs()=,
    '       gChord()..., gNoteName()...
    ' Globals set:

    ' Write the ChordRecs by DblTypes to a file
    
    ' cmdWriteAll_click changes the boolean value
    ' of gbWriteAll.
    ' If false,
    '   sub WriteDblTypes
    ' writes only one file for the gKeyName and
    ' gKeyMode of the current song.
    ' If true,
    '   Sub WriteAllChords
    ' writes a file for each gKeyName in this gKeyMode.
    
    ' If gCurrSong has modulation keys K2:-K4:
    ' then create a new chord file for the gKeyName
    ' and gKeyMode used for the place in the upper grid
    ' you are at. 4aug08
    
    Dim FileNo As Integer
    Dim ScaleDeg As Integer
    Dim TotLineOut As Integer
    Dim LineOut As Integer
    Dim ChordLine As Integer
    Dim DblNo As Integer
    Dim CrdNo As Integer
    Dim ChordFirst As Integer
    Dim ChordLast As Integer
    Dim I As Integer
    Dim bAllTitles As Boolean
    Dim bSkipHeader As Boolean
    Dim Prefix As String
    Dim File As String
    Dim A$
    Dim B$
    Dim C$
    Dim Q$
    Dim E$
        
    Dim CrdMode As Variant
    Dim Qualty As Variant
    
    Dim ThisNumKey
    Dim ThisKeyChordFirst As Integer
    Dim ThisKeyChordLast As Integer
    Dim gKeyNameSave As String
    Dim gKeyModeSave As Integer
    Dim gSongKeySave As Integer
    Dim Note As Integer
    Dim N As Integer
    Dim SKey As Integer
    Dim SDeg As Integer
    
'********************************************
'********************************************
Main:
    GoSub Init
    gKeyNameSave = gKeyName
    gKeyModeSave = gKeyMode
    gSongKeySave = gSongKey
    GoSub GetNewKeyName
    GoSub Init2
    'GoSub Debug1
    GoSub MainLine
    gKeyName = gKeyNameSave
    gKeyMode = gKeyModeSave
    gSongKey = gSongKeySave
    GoSub Conclusion
    Exit Sub

'********************************************
'********************************************

Init:
    CrdMode = Array("", "Major", "nat Minor", "harm Minor", _
        "mel Minor")
    Qualty = Array("", "Major", "minor", "dim", "augm")
    
    bAllTitles = True
    'bAllTitles = False
    
    ' Do not write out the headers if you want to
    ' read the data lines into EXCEL, e.g. for
    ' sorting on the note names to look for dupes.
    'bSkipHeader = True
    
    txtPrint.Text = ""
    'Prefix = "C:\vb4prg\4part\chords\"
    Prefix = gPrefix & gDirChords
    Return
       
'********************************************

Init2:
    Select Case gKeyMode
    Case MAJOR
        File = gKeyName & "Major" & ".TXT"
    Case MINORNAT, MINORHARM, MINORMELUP
        File = gKeyName & "Minor" & ".TXT"
    Case DORIAN
        File = gKeyName & "Dorian" & ".TXT"
    Case PHRYGIAN
        File = gKeyName & "Phrygian" & ".TXT"
    Case LYDIAN
        File = gKeyName & "Lydian" & ".TXT"
    Case MIXOLYDIAN
        File = gKeyName & "Mixolydian" & ".TXT"
    Case Else
    End Select
    gbDebugPrintSave = gbDebugPrint
    gbDebugPrint = True

    FileNo = FreeFile ' get next available #
    Open Prefix & File For Output As #FileNo
    If gbDebugPrint Then
        'Debug.Print "Opened file="; File
    End If
    Return

'********************************************
' Following 2 lines taken from sub DisplayCandChords
'   N = gNoteX - gSong(gSongCurr).FirstNote
'   txtNoteNo.Text = Str$(N + 1)
' I get the gSongKey for the note that is displayed
' left of the lower grid in the "Next Note" cmd box.
' I use that gSongKey to get the related gKeyname and
' gKeyMode, to put out a file only for that key.
GetNewKeyName:
    N = Val(txtNoteNo.Text) - 1
    Note = N + gSong(gSongCurr).FirstNote
    gSongKey = gNote(Note).SongKey
    With gSong(gSongCurr)
        gKeyName = .Key(gSongKey).Name ' eg. C, C#
        ' Add a blank to fixed string *2
        ' gsong().key().name so that
        ' it can be trimmed to a normal string.
        gKeyName = Trim$(gKeyName & " ")
        gKeyMode = .Key(gSongKey).Mode ' 1=Major,...
    End With
    ThisKeyChordFirst = gKeyChords(gSongKey).FirstChord(1)
    ThisKeyChordLast = gKeyChords(gSongKey).FinalChord(7)
    Return
    
'********************************************

MainLine:
    ' This sub is called once for each
    ' key to be written out.
    If gChordLastSave = 0 Then
        'ChordFirst = 1
        ChordFirst = ThisKeyChordFirst '4aug08
        ChordLast = ThisKeyChordLast
    Else
        ChordFirst = gChordLastSave
        ChordLast = gChordLast
        gChordLastSave = 0
    End If
    ChordLine = 0
    GoSub GetChords
    Return
    
'********************************************

GetChords:
    For ScaleDeg = TONIC To LEADINGTONE
        For DblNo = 1 To DBLTYPEMAX
            LineOut = 0
            If bAllTitles Then
                GoSub WriteHeader1
            End If
            'For CrdNo = ChordFirst To gChordLast
            For CrdNo = ChordFirst To ChordLast
            With gChord(CrdNo)
                If .DblType = DblNo _
                And .ScaleDeg = ScaleDeg Then
                    LineOut = LineOut + 1
                    If LineOut = 1 Then
                        If Not bAllTitles Then
                            GoSub WriteHeader1
                        End If
                        GoSub WriteHeader2
                    End If
                    ChordLine = ChordLine + 1
                    GoSub WriteFields
                    gDblType(DblNo).TotVal = gDblType(DblNo).TotVal + 1
                End If
            End With
            Next CrdNo
            TotLineOut = TotLineOut + LineOut
        Next DblNo
    Next ScaleDeg
    Return
    
'********************************************

WriteFields:
    With gChord(CrdNo)
        C$ = Left$(CrdMode(.ChordMode), 1)
        If .AlteredChord = 0 Then
            A$ = "  "
        Else
            A$ = "^" & Format(.AlteredChord)
        End If
        Q$ = Left$(Qualty(.Quality), 1)
        
        Write #FileNo, ChordLine; _
             ScaleDeg; DblNo; LineOut; _
            .Inversion; .Doubled; " "; _
            C$; A$; Q$;
            '.ChordMode; .AlteredChord; _
            '.Quality; " ";
        GoSub ConcatLetters
        Write #FileNo, A$;
        GoSub ConcatPositions
        Write #FileNo, B$;
        A$ = _
            gNoteName(.Voice(BASS).Index2Na) & _
            gNoteName(.Voice(TENOR).Index2Na) & _
            gNoteName(.Voice(ALTO).Index2Na) & _
            gNoteName(.Voice(SOPRANO).Index2Na)
        Write #FileNo, A$;
        Write #FileNo, _
            gNoteName(.Voice(BASS).Index2Na); _
            .Voice(BASS).ChordPos; _
            gNoteName(.Voice(TENOR).Index2Na); _
            .Voice(TENOR).ChordPos; _
            gNoteName(.Voice(ALTO).Index2Na); _
            .Voice(ALTO).ChordPos; _
            gNoteName(.Voice(SOPRANO).Index2Na); _
            .Voice(SOPRANO).ChordPos; " ";
        Write #FileNo, _
            .Interval(TMINUSB); _
            .Interval(AMINUSB); _
            .Interval(SMINUSB); _
            .Interval(AMINUST); _
            .Interval(SMINUST); _
            .Interval(SMINUSA); " ";
        Write #FileNo, _
            fcnbPrtBool(.bBassClose, _
                "BC", "BO"); _
            fcnbPrtBool(.bTriadClose, _
                "TC", "TO"); ' call
        Write #FileNo, _
            .TriadMean; .TriadStdDev; _
            .ChordMean; .ChordStdDev
    End With
    Return

'********************************************
    
WriteHeader1:
    If Not bSkipHeader Then
        Write #FileNo, ScaleDeg; _
            DblNo; _
            gDblNames(DblNo); _
            ": "; _
            gDblDescs(DblNo)
    End If
    Return
    
'********************************************
    
WriteHeader2:
    If Not bSkipHeader Then
        Write #FileNo, "C#"; "SD"; "DT"; _
            "LN"; "NV"; "DB"; " "; _
            "CM"; "AL"; "QL"; " "; _
            "BTAS"; "POS"; "BTAS2"; _
            "Bass"; "Ch"; "Tenr"; "Ch"; _
            "Alto"; "Ch"; "Sopr"; "Ch"; " "; _
            "TB"; "AB"; "SB"; "AT"; "ST"; "SA"; " "; _
            "BX"; "TX"; _
            "TMean"; "TSDev"; _
            "CMean"; "CSDev"
    End If
    Return
    
'***********************************

' Strip off the digits and concatenate
' the note name letters and accidentals
' for ease in searching.
ConcatLetters:
    With gChord(CrdNo)
        A$ = ""
        B$ = ""
        For I = BASS To SOPRANO Step -1
            C$ = gNoteName(.Voice(I).Index2Na)
            B$ = B$ & Right$(C$, 1) ' concat digit
            C$ = Left$(C$, Len(C$) - 1)
            A$ = A$ & C$ ' concat letter + accid
        Next I
    End With
    Return

'***********************************

' Concatenate the chord positions (1,3,5,7)
' for ease in searching.
ConcatPositions:
    With gChord(CrdNo)
        B$ = ""
        For I = BASS To SOPRANO Step -1
            B$ = B$ & Format(.Voice(I).ChordPos)
        Next I
    End With
    Return

'********************************************
    
Conclusion:
    If Not bSkipHeader Then
        Write #FileNo, "Grand Total chords checked="; _
            gTotChordsChecked; _
            "; Valid="; _
            gTotChordsValid
        For I = 1 To DBLTYPEMAX
            Write #FileNo, I; gDblType(I).TotVal; _
                gDblDescs(I)
        Next I
    End If

    Close #FileNo    ' Close file.
    
    E$ = "WriteDblTypes: Closed file=" & File & _
        " TotLineOut=" & TotLineOut
    If gbDebugPrint Then
        Debug.Print E$
    End If
    gbDebugPrint = gbDebugPrintSave
    txtPrint.Text = E$
    'Exit Sub
    Return
    
'********************************************

' Print the ranges of each SDeg within each of the
' up to four SKey's, to inspect the count of the
' chord table.
' Notice that the size of the chord tables can grow
' very big depending on the ranges in the options, e.g.
'    OP: RANGE   SOPRANO    G4-A5
Debug1:
    ThisNumKey = gSong(gSongCurr).NumKey
    For SKey = 1 To ThisNumKey
        With gKeyChords(SKey)
            Debug.Print "gKeyname gKeymode="; _
                gKeyName; gKeyMode
            Debug.Print "WriteDblTypes:D1 "; _
                "SKey,Sdeg,FirstChord="; SKey;
            For SDeg = TONIC To LEADINGTONE ' 1 to 7
                Debug.Print SDeg; .FirstChord(SDeg);
            Next SDeg
            Debug.Print
        
            Debug.Print "WriteDblTypes:D1 "; _
                "SKey,Sdeg,FinalChord="; SKey;
            For SDeg = TONIC To LEADINGTONE ' 1 to 7
                Debug.Print SDeg; .FinalChord(SDeg);
            Next SDeg
            I = .FinalChord(7) - .FirstChord(1)
            Debug.Print
            Debug.Print "Skey diff="; I
            Debug.Print
        End With
    Next SKey
    I = I
    Return

End Sub

Private Sub cmdChords_Click()
    ' called by cmd click - TODO was this cmd click
    '   needed for testing chord routines only?
    ' called by sub Harmonize

    ' (Calls sub WriteAllChords)
    ' calls sub InitVoiceRange
    ' calls sub InitChordsForKey
    ' calls sub MainChordsForKey

    ' Globals used: gSongCurr
    '   gSong(gSongCurr)...,
    '   gSongKey = gNote(gNoteX).SongKey was set in
    '   sub Harmonize.

    ' Globals set: gKeyName =, gKeyMode =

    ' Generate chords
    ' (1) for a new song; or
    ' (2) if chords changed for this note
    ' with Kn: on RN: card.
    ' Notice that the notes are checked backwards
    ' as usual, from the last note forward, gNoteX.
    

    ' See Subs
    '   StoreRN, StoreNotes,
    '   InputLines, FillWithKeys
    ' for
    '   gSongKey
    '   gnote(gNoteX).SongKey

    Dim I As Integer
    Dim J As Integer
    Dim bErr As Boolean

'********************************************
'********************************************
Main:
    GoSub Init
    If bErr Then
        Stop
    Else
        GoSub MainLine
    End If
    Exit Sub

'********************************************
'********************************************

Init:
    txtPrint.Text = ""
    If gSongCurr <= 0 Then
        bErr = True
        Debug.Print "cmdChords_Click: "; _
            "invalid gSongCurr="; gSongCurr
    ElseIf gSongCurr > gSongLast Then
        bErr = True
        Debug.Print "cmdChords_Click: "; _
            "invalid gSongCurr="; gSongCurr
    Else
        bErr = False
    End If
    Return

'********************************************

    ' TODO do not create new chords for a new song if
    ' its keys match all of the previous song's keys.
MainLine:
    'With gSong(gSongCurr) 9may08
        If gSongKey <> gNote(gNoteX).SongKey Then
            If gSongKey = -1 Then ' set in sub
            ' cmdHarmony_click to indicate a new song
                gChordLast = 0
                gChordLastUpper = UPPERCHORDSMIN
                GoSub InitKeyChords
            End If
            gSongKey = gNote(gNoteX).SongKey
            GoSub GetNewKeyName
            GoSub BuildChordsForKey
        End If
    'End With
    Return

'********************************************
    
    ' .FinalChord is the index of  the last chord
    ' in gChord of this scale degree.
InitKeyChords:
    For J = 1 To NUMKEYMAX ' 4
        For I = TONIC To LEADINGTONE
            gKeyChords(J).FinalChord(I) = 0
        Next I
    Next J
    Return

'********************************************

GetNewKeyName:
    With gSong(gSongCurr)
        gKeyName = .Key(gSongKey).Name ' eg. C, C#
        ' Add a blank to fixed string *2
        ' gsong().key().name so that
        ' it can be trimmed to a normal string.
        gKeyName = Trim$(gKeyName & " ")
        gKeyMode = .Key(gSongKey).Mode ' 1=Major,...
    End With
    Return

'********************************************

' Added to sub cmdHarmony_click
BuildChordsForKey:
    Call InitChordsForKey
    
    ' Bypass building the chord table for this key
    ' if it was built before for this song.
    If gKeyChords(gSongKey).FinalChord(TONIC) = 0 Then
        Call MainChordsForKey ' build chord tables
    End If
    'gKeyNamePrev = gKeyName
    'gKeyModePrev = gKeyMode
    Return

'********************************************
' TODO cleanup
' Not used. Moved to Sub cmdHarmony_Click()
    'If Left$(.T, 16) = "Write all chords" Then
    '    call WriteAllChords
    'Else
    'End If

End Sub

Private Sub ParseAbc()
    ' called by sub InputLines
    ' calls sub ParseAbcNote

    ' Globals used: gDblNames()=, gDblDescs()=,
    '       gChord()..., gNoteName()...
    ' Globals set:

    ' If option OP: MISC    ABCINPUT    ON
    ' then Sub InputLines concatenates into
    ' gAbcInput(gAbcVoice) the abc data for each
    ' voice n from the (midi2abc) ABC note lines
    ' following each V:n card met.  See Debug4.

    ' The ABC program midi2abc converted the midi file
    ' for the Bach chorale R156
    ' or file 000306b.txt
    ' or file B3.6
    ' into strings following each V:n card.
    ' Sub InputLInes concatenated them into a
    ' string gAbcInput(n) for each voice n:
    ' 1 z6A2|A2 A2 FG A2|BA G2 F2 F2|G2 A2 B2 EF|GA ...
    ' 2 z6CD|E2 A,2 D2 C2|B,2 E2 ^D2 ^D2|E2 F2 F ^D1...
    ' 3 z6A,B,|CD E2 A,2 A,G,|F,2 G,A, B,2 B,2|B,2 ^'''
    ' 4 z6A,2|A,,B,, C,2 D,E, F,E,|^D,2 E,2 B,,2 B,A...

    ' At the end of all the lines of ABC input for
    ' a song, gbABCSongEnd is set in sub StoreSong.
    ' Then sub InputLines calls this sub ParseABC
    ' to process all the ABC lines.
    ' Notice that the output of the midi2abc program
    ' looks like this for each voice:
    '   K:G % 1 sharps
    '   V:1
    '   M: 3/4
    '   L: 1/8
    '   K:G % 1 sharps
    '   Q:1/4=88
    '   G2|G4d2|B3 AG2|G3 AB2|
    '   A4B2|d4c2|B2A4|G4G2| ...
    '   V:2
    '   etc
    ' The sub InputLines collects all the lines for
    ' each voice into four strings:
    ' 1 z4G2|G4d2|B3 AG2| ...
    ' 2 z4D2|D2E2D2|D4B,2|EDE ...
    ' 3 z4B,2|B,2C B,A,2|G,2F,2G, ...
    ' 4 z4G,,2|G,2E,2F,2|G,2D,2E,2|C,2B, ...
    
    ' Then the sub ParseABC converts the ABC format notes
    ' into the 4Part format notes, separating the
    ' note names and note lengths, into 9 lines:
    
    ' 1 V1: z   G4  |   G4 ...
    ' 1 T1: E4  E2  |   E4  E2
    ' 2 V2: z   D4  |   D4  E4  D4
    ' 2 T2: E4  E2  |   E2  E2  E2
    ' 3 V3: z   B3  |   B3  C4  B3 ...
    ' 3 T3: E4  E2  |   E2  E
    ' 4 V4: z   G2  |   G3  E3  F3  |   G3
    ' 4 T4: E4  E2  |   E2
    ' 5 TM: E4  E2  |   E2  E ...

    ' Finally, sub InputLines calls sub PushBackLine
    ' to store these 9 lines in reverse order in array
    ' gInputStack.  Sub InputLines will immediately
    ' get these lines in the proper order into
    ' gTextLine and process them normally,
    ' as if input by the user manually.
    
    Dim J As Integer
    Dim K As Integer
    Dim Pos As Integer
    Dim A$
    Dim B$
    Dim C$
    Dim D$

    Dim I As Integer
    Dim bErr As Boolean
    Dim Voice As Integer
    Dim LowestPos As Integer
    Dim LowestLen As Integer
    Dim LowestVoice As Integer
    Dim PrevLowestPos As Integer
    Dim V$
    Dim T$
    Dim Ly$
    Dim TMUnits As Integer
    Dim TMFourPart$
    Dim NoteNo As Integer
    Dim bBarType As Boolean
    Dim bDebug As Boolean
    Dim Tilde$

'********************************************
'********************************************

Main:
    bDebug = True
    'bDebug = False
    GoSub Init
    GoSub GetFirstNotes
    GoSub MainLoop
        'GoSub GetNextBars
    'Stop
    I = I
    Exit Sub

'********************************************
'********************************************

Init:
    GoSub Debug4 ' if bDebug, print the raw ABC notes
    ' Initialize the output strings from the
    ' conversion from ABC format to 4Part format.
    ' VB Function Format(2)="2" with no leading blank
        
    ' Notice that the ABCPlus program midiabc
    ' will convert the DUWBC midi file
    ' (Dminican University Winifred Baker Chorale),
    ' produced by Noteworthy Composer,
    ' eg beetB.mid to beetB.abc.
    ' Then my VB program MyFile2.vbp
    ' will convert the beetB.abc to beetB2.abc,
    ' correcting the Noteworthy Composer output
    ' to a standard ABCPlus format that my
    ' program 4Part.vbp can read. 29aug08
    
    ' In the midi2abc output from beetB.mid,
    ' if there is a single lyric syllable, it precedes
    ' one or more notes.  If more than one note
    ' is on the line following a lyric syllable,
    ' then my program MyFile2.vbp will add " _"
    ' for each extra note.
    
    ' If OP: MISC ABCINPUT ON,
    ' assume that the lyric lines that are input after
    ' each "V:n" all begin with "i:", as created by
    ' my program MyFile2.vbp.
    
    ' If V:1 is input on a line, then gAbcVoice = 1,
    ' and the lines following it without any
    ' identifying symbol are concatenated into
    ' gAbcInput(gAbcVoice).string.
    
    ' Similarly, any "i:" lines following "V:1",
    ' eg "i:Hal", are concatenated into the
    ' gAbcInputLyric(gAbcVoice).string
    ' with a leading space, as ="i: Hal".
    
    ' Now for Voice=1 or Soprano,
    '           gAbcI(Voice).String = '11:"
    ' Now for Voice=4 or Bass,
    '           gAbcI(Voice).String = '41:"
    
    ' Use of 21: 31: or 41: will set
    '   gbOneLyricLinePerVoice = True
    ' in sub StoreNotes.
    
    Tilde = "~"
    For Voice = SOPRANO To BASS
        gAbcV(Voice).String = "V" & Format(Voice) & ":"
        gAbcT(Voice).String = "T" & Format(Voice) & ":"
        gAbcI(Voice).String = Format(Voice) & "1:"
    Next Voice
    gAbcTM.String = "TM:"
    gAbcNoteNo.String = "%"
    gAbcMeasNo.String = "%1"
        'gAbcI(Voice).String = "1" & Format(Voice) & ":"
    
    For Voice = SOPRANO To BASS
        gLastBarPos(Voice) = 0
        gBarsFound(Voice) = 0
        With gAbc(Voice)
            .Pre.Abc = ""
            .Pre.Accid = ""
            .Pre.FourPart = ""
            .Pre.Pos = 0 ' only one used of .Pre
            .Pre.Len = 0

            .Name.Abc = ""
            .Name.Accid = ""
            .Name.FourPart = ""
            .Name.Pos = 0
            .Name.Len = 0

            .Time.Abc = ""
            .Time.Accid = ""
            .Time.FourPart = ""
            .Time.Pos = 0
            .Time.Len = 0

            .Post.Abc = ""
            .Post.Accid = ""
            .Post.FourPart = ""
            .Post.Pos = 0
            .Post.Len = 0
            
            .bBarNote = False
            .MeasNo = 1
            '.MeasPos = 0
            '.NoteNo = 0
            '.NoteLen = 0
            .bBeamToNext = False
            .Lyric(1) = ""
        End With
    Next Voice
    Return

'********************************************

    ' For each voice, one note at a time,
    ' get the next ABC format note name, len, etc,
    ' e.g. ^C''' or B, and store in
    '   gAbc(pVoice).Name.Abc
    '   gAbc(pVoice).Name.Len etc
    ' Then convert it to 4Part format name, len, etc,
    ' e.g. C#3 and store in
    '   gAbc(pVoice).Name.FourPart
    '   gAbc(pVoice).Name.Len etc
    
    ' Do the same with the .Time fields.
GetFirstNotes:
    For Voice = SOPRANO To BASS
        If gAbcInput(Voice).bUsed Then
            Call ParseAbcNote(Voice, bErr)
        End If
    Next Voice
    Return

'********************************************

MainLoop:
    GoSub GetLowestPos
    Do While LowestPos < MAXINTEGER ' EOS
        GoSub PutLowestNotes
        GoSub GetLowestPos
    Loop
    If LowestPos = MAXINTEGER Then ' EOS
        GoSub FinalBars
    End If
    Return

'********************************************

GetLowestPos:
    'If gAbc(1).MeasNo = 4 Then
    '    I = I ' testing
    'End If
    PrevLowestPos = LowestPos
    LowestPos = MAXINTEGER
    LowestLen = MAXINTEGER
    'LowestVoice = 0
    ' First find the lowest position in the
    ' measure for the next note to output.
    ' For all the voices with the same lowest
    ' position in the measure, choose the one
    ' with the shortest length.
    ' At present, NOT using
    '   LowestLen
    '   LowestVoice
    ' except to find LowestPos.
    ' TODO 12/14/6
    For Voice = SOPRANO To BASS
        If gAbc(1).MeasNo >= 20 Then
            If Voice = 1 Then
                'GoSub Debug6A
            End If
            'GoSub Debug6
        End If
        If gAbcInput(Voice).bUsed Then
            If gAbc(Voice).Time.Pos = MAXINTEGER Then
                Return
            ElseIf gAbc(Voice).Time.Pos < LowestPos Then
                LowestPos = gAbc(Voice).Time.Pos
                LowestLen = gAbc(Voice).Time.Len
                'LowestVoice = Voice
            ElseIf gAbc(Voice).Time.Pos = LowestPos Then
                If gAbc(Voice).Time.Len < LowestLen Then
                    LowestPos = gAbc(Voice).Time.Pos
                    LowestLen = gAbc(Voice).Time.Len
                    'LowestVoice = Voice
                End If
            Else ' already the lowest .Time.Pos
                I = I
            End If
        Else
            I = I
        End If
    Next Voice
    'GoSub Debug5
        If LowestPos = MAXINTEGER Then
           I = I
        End If
        If LowestLen = MAXINTEGER Then
           I = I
        End If
    Return
    
'********************************************
    
    ' TODO 12/14/6 not used
    For Voice = SOPRANO To BASS
        'GoSub Debug6
        If gAbc(Voice).Time.Pos < LowestPos Then
            LowestPos = gAbc(Voice).Time.Pos
        End If
    Next Voice

    ' For all the voices with the same lowest
    ' position in the measure, choose the one
    ' with the shortest length.
    ' At present, NOT using:
    '   LowestLen
    '   LowestVoice
    For Voice = SOPRANO To BASS
        If gAbc(Voice).Time.Pos = LowestPos Then
            If gAbc(Voice).Time.Len < LowestLen Then
                LowestLen = gAbc(Voice).Time.Len
                'LowestVoice = Voice
            End If
        End If
    Next Voice
    'GoSub Debug5
    Return

'********************************************

Debug5:
    'if not bdebug Then Return
    Debug.Print "ParseAbc:D5"
    'Debug.Print
    Debug.Print gAbc(1).MeasNo; LowestVoice; _
        gAbc(LowestVoice).Name.Abc; _
        LowestPos; LowestLen
    Return

'********************************************

Debug6A:
    'if not bdebug Then Return
    Debug.Print "ParseAbc:D6 voice,measno,"; _
        "gAbc(V).Name.Abc, .time.pos, .time.len="
    Return
    
Debug6:
    Debug.Print "D6: "; Voice; gAbc(1).MeasNo; _
        gAbc(Voice).Name.Abc; _
        gAbc(Voice).Time.Pos; _
        gAbc(Voice).Time.Len; "/ "
    Return

'********************************************

    ' After the next Lowest Pos note or notes
    ' in the chord, e.g. 0, 4, 6, 8, etc,
    ' are concatenated to the 4Part string for
    ' that voice, sub ParseAbcNote gets the
    ' next note for that voice or voices into
    '   gAbc(pVoice).Name.Abc
    '   gAbc(pVoice).Name.FourPart
PutLowestNotes:
    'For Voice = LowestVoice To BASS
    For Voice = SOPRANO To BASS
        If gAbcInput(Voice).bUsed Then
            If gAbc(Voice).Time.Pos = LowestPos Then
                GoSub Put4PartNote
                GoSub Init2
                ' Get the next note for this voice:
                Call ParseAbcNote(Voice, bErr)
                'GoSub Debug2
                If bErr Then
                    Stop
                End If
            ElseIf gAbc(Voice).Time.Pos < LowestPos Then
                Stop
            'ElseIf gAbc(Voice).Time.Pos >= LowestPos + 32 Then
            '    Stop
            Else
                GoSub AddTabToAlignVertically
            End If
        Else
            I = I
        End If
    Next Voice
    ' In each measure, start with the 2nd note
    ' to calculate the TM: card's note's length.
    ' That is, the notes output on the TM: card
    ' lag those on the T1:-T4: cards by one note.
    ' Catch up at the next bar note.
    If LowestPos > 0 Then ' past 1st note in meas
        GoSub TM4PartUnits
        If bBarType Then
            gAbcTM.String = gAbcTM.String & _
                Chr$(9) & "|"
            LowestPos = 0
            PrevLowestPos = 0
            NoteNo = NoteNo + 1
            A$ = Format(gAbc(1).MeasNo)
            'gAbcNoteNo.String = gAbcNoteNo.String & _
            '    Chr$(9) & "|" & Format(NoteNo)
            gAbcNoteNo.String = gAbcNoteNo.String & _
                Chr$(9) & "|m:" & A$
            'A$ = ":" & Format(gAbc(1).MeasNo)
            gAbcMeasNo.String = gAbcMeasNo.String & _
                Chr$(9) & "|" & A$
        End If
    End If
    Return

'********************************************

AddTabToAlignVertically:
    gAbcV(Voice).String = gAbcV(Voice).String & Chr$(9)
    gAbcT(Voice).String = gAbcT(Voice).String & Chr$(9)
    If gAbcInputLyric(Voice).bUsed Then
        gAbcI(Voice).String = gAbcI(Voice).String & Chr$(9)
    End If
    Return
    
'********************************************

' Calculate the length of the TM: card note
' from LowestPos - PrevLowestPos
TM4PartUnits:
    TMUnits = LowestPos - PrevLowestPos
    GoSub ConvertTMUnitsTo4Part
    gAbcTM.String = gAbcTM.String & _
        Chr$(9) & TMFourPart$
    NoteNo = NoteNo + 1
    gAbcNoteNo.String = gAbcNoteNo.String & _
        Chr$(9) & Format(NoteNo)
    gAbcMeasNo.String = gAbcMeasNo.String & _
        Chr$(9)
    'Debug.Print PrevLowestPos; LowestPos; gAbcTM.String
    Return

'********************************************

ConvertTMUnitsTo4Part:
' In case a note name has no length following.
' E.g. if L: 1/8 @WN=32U,
' then .Default="E" and .LUnits=4
' See DefaultTime: in sub ParseAbcNote
    TMFourPart$ = _
        LCase$(gSong(gSongLast).L.Default) ' e
    I = gSong(gSongLast).L.LUnits ' 4
    If TMUnits = I Then
        ' E
    ElseIf TMUnits Mod I = 0 Then
        J = TMUnits / I
        TMFourPart$ = TMFourPart$ & Format(J)
    ElseIf I Mod TMUnits = 0 Then
        J = I / TMUnits ' if abc=e2, then j=4/2
        J = J * gSong(gSongLast).L.Denom ' eg =8 for 1/8
        D$ = "1/" & Format(J) ' eg 1/16 for "S"
        Call ParseAbc4PartTime(D$, TMFourPart$)
    Else
        TMFourPart$ = TMFourPart$ & _
            Format(TMUnits) & "/" & Format(I)
    End If
    Return

'********************************************

' Insert a TAB char Chr$(9) before every field
' in the 4Part format.
Put4PartNote:
    GoSub Test2
    V$ = Chr$(9) & _
        gAbc(Voice).Pre.Abc & _
        gAbc(Voice).Name.FourPart & _
        gAbc(Voice).Post.Abc
    V$ = Trim(V$)
    
    ' To write out z* and -* in file g4PartFile.
    If Voice = SOPRANO Then
        If Right$(V$, 1) = "z" Then
            V$ = V$ & "*" ' "z*" 15dec08
        ElseIf Right$(V$, 1) = "-" Then
            V$ = V$ & "*" ' "-*"
        End If
    End If
    
    gAbc(Voice).Pre.Abc = ""
    gAbc(Voice).Post.Abc = ""
    gAbcV(Voice).String = gAbcV(Voice).String & V$
    
    T$ = Chr$(9) & gAbc(Voice).Time.FourPart
    T$ = Trim$(T$)
    gAbcT(Voice).String = gAbcT(Voice).String & T$
    If gAbc(Voice).bBeamToNext Then
        gAbcT(Voice).String = _
            gAbcT(Voice).String & Tilde
    End If
    gAbc(Voice).Name.Accid = ""
    
    ' TAB failed when Ly$ = Chr$(9) & "_" (?)
    ' so use two separate .String = 's.
    Ly$ = Chr$(9) & gAbc(Voice).Lyric(1)
       
    If gAbcInputLyric(Voice).bUsed Then
      gAbcI(Voice).String = _
          gAbcI(Voice).String & Chr$(9)
      gAbcI(Voice).String = _
          gAbcI(Voice).String & gAbc(Voice).Lyric(1)
        If gAbc(Voice).Lyric(1) = "z" Then
            I = I
        End If
          gAbc(Voice).Lyric(1) = "" ' 19sep08
    End If
    'GoSub Test2
    'GoSub Test1
    Return
'********************************************

Test2:
    If gAbc(Voice).Name.FourPart = "|" Then
      If Voice = 3 Or Voice = 4 Then
            I = I
      End If
    End If
    Return
    
    If NoteNo >= 18 Then
        If Voice = 1 Or Voice = 2 Then
            GoSub Debug1
            Debug.Print "noteno="; NoteNo
            I = I
        End If
    End If
    Return
      
'********************************************

' USABLE
Test1:
    If Voice = 1 Then
        'GoSub Debug1
        If Right$(V$, 1) = "|" Then
            I = I
        End If
    End If
      
    If gAbc(Voice).Name.FourPart = "D4" Then
        I = I
    End If
      
    If Right$(gAbcV(Voice).String, 2) = "D4" Then
        I = I
    End If
    If gAbc(Voice).Time.FourPart = "" Then
        I = I ' testing
    End If
            
    'If Voice = LowestVoice Then
    '    gAbcTM.String = gAbcTM.String & T$
    'End If
    ' TODO 12/15/6

    Return
      
'********************************************

' I believe the point of this routine is to add
' double bars to the end of string of ABC notes.
' Apparently, midi2ABC doesn't add a final bar or
' doublebar. 01aug08

' I needed to add a space before the || in case
' the string is empty, eg when only the Soprano line
' is input from an ABC source file. 23aug08
'       gAbcV(Voice).String & " ||"
'       gAbcT(Voice).String & " ||"

FinalBars:
    A$ = ""
    If bDebug Then
        Debug.Print "ParseAbc:D1"
    End If
    GoSub FinalVoiceBars
    GoSub FinalTnBars
    GoSub FinalTMBars
    Return

'********************************************

FinalVoiceBars:
      For Voice = SOPRANO To BASS
            'B$ = gAbc(Voice).Name.FourPart
            'C$ = Left$(B$, 1)
            B$ = Right$(gAbcV(Voice).String, 2)
            C$ = Right$(gAbcV(Voice).String, 1)
            If B$ = "||" Then
                ' good
            ElseIf C$ = "|" Then
                gAbcV(Voice).String = _
                    gAbcV(Voice).String & "|"
            ElseIf Len(gAbcV(Voice).String) <= 3 Then
                gAbcV(Voice).String = ""
            Else
                gAbcV(Voice).String = _
                    gAbcV(Voice).String & "||"
            End If
      Next Voice
      Return
    
'********************************************

FinalTnBars:
      For Voice = SOPRANO To BASS
            B$ = Right$(gAbcT(Voice).String, 2)
            C$ = Right$(gAbcT(Voice).String, 1)
            If B$ = "||" Then
                ' good
            ElseIf C$ = "|" Then
                gAbcT(Voice).String = _
                    gAbcT(Voice).String & "|"
            ElseIf Len(gAbcT(Voice).String) <= 3 Then
                gAbcT(Voice).String = ""
            Else
                gAbcT(Voice).String = _
                    gAbcT(Voice).String & "||"
            End If
            GoSub Debug1
      Next Voice
      Return
    
'********************************************

FinalTMBars:
      B$ = Right$(gAbcTM.String, 2)
      C$ = Right$(gAbcTM.String, 1)
      If B$ = "||" Then
          ' good
      ElseIf C$ = "|" Then
          gAbcTM.String = _
              gAbcTM.String & "|"
      ElseIf Len(gAbcTM.String) <= 3 Then
          gAbcTM.String = ""
      Else
          gAbcTM.String = _
              gAbcTM.String & "||"
      End If
      GoSub DebugTM
      Return
    
'********************************************

' If this IS a BARNOTE then the notes
' in the next measure all start out at
' time position = 0.
' Update the time position for the
' next note for this voice:
Init2:
    If gAbc(Voice).bBarNote Then
        gAbc(Voice).Time.Pos = 0
        gAbc(Voice).bBarNote = False
        bBarType = True
        If Voice = 1 Then
            I = I
            'gAbc(Voice).MeasNo = _
                gAbc(Voice).MeasNo
        End If
    Else
        If gAbc(Voice).Time.Pos > 32 Then
            GoSub Debug3
            GoSub Debug1
            GoSub Debug6
            I = I
        End If
        gAbc(Voice).Time.Pos = _
            gAbc(Voice).Time.Pos + _
            gAbc(Voice).Time.Len
        bBarType = False
    End If
    'GoSub Debug3
    Return

'********************************************

Debug1:
    'If Not bDebug Then Return
    'Debug.Print "ParseAbc:D1"
    If Len(gAbcV(Voice).String) > 0 Then
        Debug.Print Voice; gAbcV(Voice).String
    End If
    If Len(gAbcT(Voice).String) > 0 Then
        Debug.Print Voice; gAbcT(Voice).String
    End If
    If Len(gAbcI(Voice).String) > 2 Then
        Debug.Print Voice; gAbcI(Voice).String
    End If
    Debug.Print
    'I = I ' testing
    Return
'********************************************

DebugTM:
    If Not bDebug Then Return
    Debug.Print "ParseAbc:TM"
    Debug.Print Voice; gAbcNoteNo.String
    Debug.Print Voice; gAbcTM.String
    I = I ' testing
    Return

'********************************************

Debug2:
    'if not bdebug Then Return
    Debug.Print "ParseAbc:D2"
    I = Voice: With gAbc(I).Name: Debug.Print I; _
        .Abc; .Pos; .FourPart; .Len: End With
    I = Voice: With gAbc(I).Time: Debug.Print I; _
        .Abc; .Pos; .FourPart; .Len: End With
    Debug.Print "next input note for voice="; Voice; _
        "is at pos="; gAbcInput(I).Pos
    I = I ' testing
    Return

'********************************************

Debug3:
    'if not bdebug Then Return
    Debug.Print "ParseAbc:D3"
    Debug.Print Voice; gAbc(Voice).Time.Pos; _
        gAbc(Voice).Time.Len
    Return

'********************************************

Debug4:
    If Not bDebug Then Return
    Debug.Print "ParseAbc:D4"
    For Voice = SOPRANO To BASS
        If Len(gAbcInput(Voice).String) > 0 Then
            Debug.Print Voice; gAbcInput(Voice).String
        End If
    Next Voice
    Debug.Print "ParseAbc:D4"
    For Voice = SOPRANO To BASS
        If Len(gAbcInputLyric(Voice).String) > 2 Then
            Debug.Print Voice; gAbcInputLyric(Voice).String
        End If
    Next Voice
    Return

End Sub
Private Sub ParseAbcNote(pVoice As Integer, _
                        pbErr As Boolean)
    ' called by sub ParseAbc

    ' calls sub ParseAbcNoteName
    ' calls sub ParseAbcNoteTime
    ' calls sub GetAbcString
    ' calls function fcnGetAbcCh
    ' calls sub PushAbcCh
    
    ' See sub InitAbcTable
    ' Globals used: gxAbcTable()
    ' Globals set:

    Dim I As Integer
    Dim bErr As Boolean
    Dim Char As String
    Dim Char2 As String
    Dim TypeCh As Integer
    Dim A$
    Dim B$
    Dim C$
    Dim Flag As Integer
    Dim Numer As Integer
    Dim Denom As Integer
    Dim E$

'********************************************
'********************************************

Main:
    GoSub Init
    GoSub MainLoop
    Exit Sub

'********************************************
'********************************************

Init:
    'gAbc(pVoice).bBarNote = False
    Return

'********************************************

MainLoop:
    gAbcPrevState = NOT_NOTE
    Do While fcnGetAbcCh(pVoice, Char) <> EOS ' call
        ' TODO 12/17/6
        TypeCh = gxAbcTable(Asc(Char))
        If pVoice = 3 Or pVoice = 4 Then
            If Char = "|" Then
                  I = I
            End If
        End If
        If pVoice = 3 Then
            I = I
            'Debug.Print TypeCh; 0; "/"; Char; "/"
        End If
        If Char = " " Then
            Call GetAbcString(pVoice, Char, _
                ASPACE, SAMECHAR, A$, bErr) ' 7
                I = I
            ' do beaming if two consecutive
            ' notes are 1/8 notes or shorter
            'Exit Do
        Else
            'TypeCh = gxAbcTable(Asc(Char))
            Select Case TypeCh
                Case OBRACKET
                    GoSub EditOBracket
                    Call ParseAbcNoteBar(pVoice, _
                        Char, TypeCh, bErr)
                Case Is < 10
                    GoSub EditPreNote
                Case Is < 12
                    Call ParseAbcNoteAccid(pVoice, _
                        Char, TypeCh, bErr)
                Case Is < 20
                    Call ParseAbcNoteName(pVoice, _
                        Char, TypeCh, bErr)
                    ' only one name field per note
                    'gAbcState = NOTE_TIME
                Case Is < 30
                    Call ParseAbcNoteTime(pVoice, _
                        Char, TypeCh, bErr)
                    ' only one time field per note
                    'gAbcState = POST_NOTE
                Case Is < 40
                    GoSub EditPostNote
                Case Is < 50
                    'GoSub EditBars
                    Call ParseAbcNoteBar(pVoice, _
                        Char, TypeCh, bErr)
                    ' only one bar field per note
                Case Is < 60
                    GoSub EditEndSong
                Case Else
            End Select
            
            If gAbcState = BAR_NOTE Then
                Exit Do
            ElseIf gAbcState = END_NOTE Then
                'GoSub CheckBeaming
                Call PushAbcCh(pVoice, Char, bErr)
                Exit Do
            ElseIf gAbcState < gAbcPrevState Then
                gAbcState = END_NOTE
                'GoSub CheckBeaming
                Call PushAbcCh(pVoice, Char, bErr)
                Exit Do
            Else
                gAbcPrevState = gAbcState
            End If
        End If
Loop1:
    Loop
    If Char = EOS Then
        gAbc(pVoice).Time.Pos = MAXINTEGER
    Else
        I = I
        GoSub CheckBeaming
    End If
    Return

'********************************************
' NOT USED 22aug08
        If pVoice = 1 Then
            If gAbcInput(pVoice).Pos = 7 Then
                I = I
            End If
        End If
        'If gAbcState = NOTE_NAME Then
        '    If TypeCh < 20 Then
        '        Call PushAbcCh(pVoice, Char, bErr)
        '        ' push back default time=1 for a note
        '        Call PushAbcCh(pVoice, "1", bErr)
        '        GoTo Loop1
        '    End If
        'End If

'********************************************

' In the raw ABC string of notes, at least when
' produced by the ABC utility program, midi2ABC,
' notes that are right next to each other should
' be "beamed", e.g. eighth notes and shorter.
' So, get the next char, Char2, after the state
' of END_NOTE is reached.  If it is a Blank,
' then turn off beaming with
'   gAbc(pVoice).bBeamToNext = False
' Else if it is a non-blank, the beginning of the
' next note (not a Bar), then turn on beaming with
'   gAbc(pVoice).bBeamToNext = True
' Afterwards, push Char2 back on the string.
CheckBeaming:
    'Or UCase$(Char) = "z"
    If Char = " " _
    Or Char = "z" _
    Or Char = "Z" _
    Or Char = "|" Then
        gAbc(pVoice).bBeamToNext = False
    ElseIf Left$(gAbc(pVoice).Name.Abc, _
        1) = "z" Then
        Return
    ElseIf fcnGetAbcCh(pVoice, Char2) <> EOS Then ' call
        'Or UCase$(Char) = "z"
        If Char2 = " " _
        Or Char = "z" _
        Or Char = "Z" _
        Or Char2 = "|" Then
            gAbc(pVoice).bBeamToNext = False
        Else
            gAbc(pVoice).bBeamToNext = True
        End If
        Call PushAbcCh(pVoice, Char2, bErr)
    End If
    Return

'********************************************

EditOBracket:
    'If a single open bracket is followed by
    ' a bar, ABC "[|", then treat as a BAR_NOTE.
    ' Else treat as a PRE-NOTE.
    Call GetAbcString(pVoice, Char, _
        ABAR, SAMECHAR, A$, bErr) ' 41
    If Len(A$) > 1 Then
        Call ParseAbcNoteBar(pVoice, _
            Char, TypeCh, bErr)
        ' only one bar field per note
    Else
        GoSub EditPreNote
    End If
    Return

'********************************************

' May be several PRE_NOTE fields per note
' If a double quote field precedes the notename
' eg "G"B, then get chars till the next dbl quote.
' Concatenate to .Pre.Abc,
' and go back for any other Pre_Note.

' If char="(" is followed by "*" then
' store "(*", so that the slur will be
' extended by StoreVoiceVn to all voices.

EditPreNote:
    gAbcState = PRE_NOTE
    If gAbcState < gAbcPrevState Then
        GoTo EditPreNoteX
    End If
    If gAbcInput(pVoice).Pos = 9 Then
        I = I
    End If
    If Char = Chr$(34) Then ' double quote
        Call GetAbcString(pVoice, Char, _
            ADQUOTE, TILLCHAR, A$, bErr) ' 7
        If Len(A$) > 0 Then
            gAbc(pVoice).Pre.Abc = _
                gAbc(pVoice).Pre.Abc & A$
        End If
    ElseIf Char = "(" Then
            Call GetAbcString(pVoice, Char, _
                ASTERISK, SAMETYPE, A$, bErr) ' 43
            If Len(A$) > 0 Then ' store "(*"
                gAbc(pVoice).Pre.Abc = _
                    gAbc(pVoice).Pre.Abc & A$
            End If
    Else
        gAbc(pVoice).Pre.Abc = _
            gAbc(pVoice).Pre.Abc & Char
    End If
    'If gAbcState < gAbcPrevState Then
    '    Return
    'End If
    gAbc(pVoice).Pre.Pos = 0
    ' TODO before 12/14/6 ?
    'Select Case TypeCh
    '    Case 0
    '    Case Else
    'End Select
EditPreNoteX:
    Return

'********************************************

' May be several POST_NOTE fields per note.
' If char="-" is followed by "*" then
' store "-*", so that the tie will be
' extended by StoreVoiceVn to all voices.
EditPostNote:
    gAbcState = POST_NOTE
    
    If Char = "-" Then
        Call GetAbcString(pVoice, Char, _
            ASTERISK, SAMETYPE, A$, bErr) ' 43
        If Len(A$) > 0 Then ' store "-*"
            gAbc(pVoice).Post.Abc = _
                gAbc(pVoice).Post.Abc & A$
        End If
    Else
        gAbc(pVoice).Post.Abc = _
            gAbc(pVoice).Post.Abc & Char
    End If
    If gAbcState < gAbcPrevState Then
        Return
    End If
    Return

'********************************************

' May be several END_NOTE fields per note

EditBars:
    gAbcState = BAR_NOTE
    If gAbcState < gAbcPrevState Then
        Return
    End If
    Return
'********************************************

' May be several END_NOTE fields per note

EditEndSong:
    gAbcState = END_SONG
    If gAbcState < gAbcPrevState Then
        Return
    End If
    Return

End Sub

Private Sub ParseAbcNoteAccid(pVoice As Integer, _
                        pChar As String, _
                        pTypeCh As Integer, _
                        pbErr As Boolean)
    ' called by sub ParseAbcNote

    ' calls sub GetAbcString

    ' Globals used:
    ' Globals set:

    ' If entered with pTypeCh=accidental type,
    ' then get the next ABC accidental field,
    ' e.g. _ or ^, and store in
    '   gAbc(pVoice).Name.Accid = A$

    Dim I As Integer
    Dim bErr As Boolean
    Dim A$
    Dim B$
    Dim C$
    Dim Flag As Integer
    Dim Numer As Integer
    Dim Denom As Integer
    Dim E$
    Dim Err As String
    Dim Accident As String

'********************************************
'********************************************

Main:
    gAbcState = NOTE_ACCID
    ' Only one name field per note
    If gAbcState > gAbcPrevState Then
        GoSub Init
        GoSub MainLoop
    End If
    Exit Sub

'********************************************
'********************************************

Init:
    Return

'********************************************

MainLoop:
    GoSub EditNote
    Return

'********************************************

EditNote:
    Select Case pTypeCh
        Case ACCIDEN ' 11
            ' May have one _ = ^ or two _ ^
            ' accidentals before the note name.
            ' _A is bA and __B is double flat bbB
            ' ^G is #G and ^^F is double sharp xF.
            ' TODO 12/15/6 not needed?
            'If gAbc(pVoice).Name.Abc <> "" Then
            '    GoSub Err1
            '    ' force accidental to be first
            '    gAbc(pVoice).Name.Abc = ""
            'End If
            Call GetAbcString(pVoice, pChar, _
                pTypeCh, SAMECHAR, A$, bErr)
            If pChar = "=" Then
                A$ = Left$(A$, 1)
            Else
                A$ = Left$(A$, 2)
            End If
            gAbc(pVoice).Name.Accid = A$
        Case Else
    End Select
    Return

'*******************************************

    ' TODO 12/17/6 not used
Err1:
    gInputErrs = gInputErrs + 1
    E$ = "ParseAbcNoteName: invalid accidental=" & _
        pChar & _
        ". Must come before note name for " & _
        "Voice=" & pVoice & _
        "in song=" & gSongLast & _
        "at lineno=" & gInputLineNo & _
        "in ABC line=" & gTextLine
    Call DisplayText(E$)
    Debug.Print E$
    Stop
    Return

End Sub



Private Sub ParseAbcNoteName(pVoice As Integer, _
                        pChar As String, _
                        pTypeCh As Integer, _
                        pbErr As Boolean)
    ' called by sub ParseAbcNote

    ' calls sub GetAbcString
    ' calls sub AbcNoteTo4part

    ' Globals used:
    ' Globals set:

    ' If entered with pTypeCh=note name type,
    ' then get the next ABC note name field,
    ' e.g. C''' or B, and store in
    '   gAbc(pVoice).Name.Abc = A$
    ' Then convert it to 4Part format and store in
    '   gAbc(pVoice).Name.FourPart = A$

    Dim I As Integer
    Dim bErr As Boolean
    Dim A$
    Dim B$
    Dim C$
    Dim Flag As Integer
    Dim Numer As Integer
    Dim Denom As Integer
    Dim E$
    Dim Err As String
    Dim Accident As String
    Dim Loc1 As Integer

'********************************************
'********************************************

Main:
    gAbcState = NOTE_NAME
    ' Only one name field per note
    If gAbcState > gAbcPrevState Then
        GoSub Init
        GoSub MainLoop
    Else
        gAbcState = END_NOTE
    End If
    Exit Sub

'********************************************
'********************************************

Init:
    'GoSub Test1
    GoSub Test2
    Return

'********************************************

Test1:
    If pVoice = 3 Or pVoice = 4 Then
        If gAbcInputLyric(pVoice).Pos = 6 _
        Or gAbcInputLyric(pVoice).Pos = 9 _
        Or gAbcInputLyric(pVoice).Pos = 12 Then
            I = I
        End If
    End If
    Return

'********************************************

Test2:
    If pVoice = 1 Or pVoice = 3 Then
        If gAbc(pVoice).Name.Accid = "=" Then
            I = I
        End If
    End If
    Return

'********************************************

MainLoop:
    GoSub EditNote
    Return

'********************************************

' If char="z" rest is followed by "*" then
' store "z*", so that the rest will be
' extended by StoreVoiceVn to all voices.

' The single letter note name
' may be followed by zero
' or more commas, ACOMMA=13, e.g. A,,
' or zero or more single quotes, AQUOTE=13,
' E.G. c'''.
            

EditNote:
    Select Case pTypeCh
        Case ANOTE '12
            'If UCase$(pChar) = "z" Then
            If pChar = "z" Then
                Call GetAbcString(pVoice, pChar, _
                    ASTERISK, SAMETYPE, A$, bErr) ' 43
                'If Len(A$) > 0 Then ' store "z*"
                If Len(A$) > 0 Then ' store "z*"
                    'gAbc(pVoice).Name.Abc = UCase$(A$) ' Z*
                    gAbc(pVoice).Name.Abc = LCase$(A$) ' Z*
                    'gAbc(pVoice).Name.Abc = _
                        gAbc(pVoice).Name.Abc & A$
                End If
            Else
                Call GetAbcString(pVoice, pChar, _
                    ACOMMA, SAMETYPE, A$, bErr)
                gAbc(pVoice).Name.Abc = A$
            End If
            GoSub DefaultTime
            GoSub AbcNoteTo4Part
            GoSub GetLyricForThisNote
        Case Else
    End Select
    Return
      
      
     
      
'********************************************
            
' Initialized .Pos = 1 in sub InputLines at
' StoreSong1:

GetLyricForThisNote:
    'C$ = UCase$(Trim$(A$))
    C$ = Trim$(A$)
    If C$ = "z" Or C$ = "z*" Then
        If gAbcInputLyric(pVoice).bUsed Then
            'gAbc(pVoice).lyric(1) = LCase$(C$)
            gAbc(pVoice).Lyric(1) = "z" ' 14dec08
        End If
    'ElseIf C$ = "|" Then
    '  gAbc(pVoice).lyric(1) = C$
    Else
      With gAbcInputLyric(pVoice)
        If .bUsed Then
            If .Pos < Len(.String) Then
              Loc1 = InStr(Mid$(.String, .Pos), " ")
              If Loc1 > 0 Then
                  C$ = Mid$(.String, .Pos, Loc1 - 1)
                  .Pos = .Pos + Loc1 ' one past the blank
                  gAbc(pVoice).Lyric(1) = C$
              Else
                  gAbc(pVoice).Lyric(1) = ""
                  I = I
                  Stop
              End If
            End If
        End If
      End With
    End If
    Return
      
'********************************************

' In case a note name has no length following.
' E.g. if L: 1/8 @WN=32U, then:
DefaultTime:
    gAbc(pVoice).Time.Abc = _
        gSong(gSongLast).L.Text ' 1/8
    gAbc(pVoice).Time.FourPart = _
        LCase$(gSong(gSongLast).L.Default) ' e
    gAbc(pVoice).Time.Len = _
        gSong(gSongLast).L.LUnits ' 4
    Return

'********************************************

AbcNoteTo4Part:
    'gAbc(pVoice).Name.FourPart = _
    '    gAbc(pVoice).Name.Abc
    A$ = gAbc(pVoice).Name.Abc
    GoSub ConvNote
    gAbc(pVoice).Name.FourPart = A$
    Return
    'call AbcNoteTo4Part(pVoice, bErr)

'*******************************************

' Convert the ABCPlus music notation format
' to my 4Part format, e.g. C,# to C3#
ConvNote:
    If A$ = "" Then
        Return
    ElseIf Left$(A$, 1) >= "a" _
    And Left$(A$, 1) <= "g" Then
        ' good letter
    ElseIf Left$(A$, 1) < "A" _
    Or Left$(A$, 1) > "G" Then
        Return ' bad letter
    End If
    Err = ""
    B$ = "" ' separate from A$ for debugging

    ' Temporarily had stored the accidentals
    ' string in .FourPart
    Accident = gAbc(pVoice).Name.Accid
    Call AbcNoteTo4Part(pVoice, A$, Accident, _
                        gbAlto3, B$, Err)
    If Err = "" Then
        A$ = B$ ' 4Part note format eg C#4
    Else
        GoSub Err2 ' try to keep on
        I = I ' testing
    End If
    Return

'********************************************

Err2:
    bErr = True
    gInputErrs = gInputErrs + 1
    'GoSub GetNoteNo
    ' " at note no=" & B$ &
    E$ = "ParseAbcNoteName: " & _
        "invalid note=" & A$ & _
        " in SongNo=" & gSongLast & Err
    Call DisplayText(E$)
    Debug.Print E$
    Stop
    Return

End Sub


Private Sub ParseAbcNoteBar(pVoice As Integer, _
                        pChar As String, _
                        pTypeCh As Integer, _
                        pbErr As Boolean)
    ' called by sub ParseAbcNote

    ' calls sub GetAbcString
    ' (Calls sub AbcNoteTo4part)

    ' Globals used:
    ' Globals set:

    ' If entered with pTypeCh=bar or colon
    ' or bracket etc ABC types,
    ' then get the next ABC bar field,
    ' e.g. } |} :| |n etc, and store in
    '   gAbc(pVoice).Name.FourPart
    '   gAbc(pVoice).Name.Abc

    Dim I As Integer
    Dim bErr As Boolean
    Dim A$
    Dim B$
    Dim C$
    Dim D$
    Dim Flag As Integer
    Dim Numer As Integer
    Dim Denom As Integer
    Dim E$
    Dim Loc1 As Integer

'********************************************
'********************************************

Main:
    If gAbcPrevState <> NOT_NOTE Then
        gAbcState = END_NOTE
        'call PushAbcCh(pVoice, pChar, pbErr)
        Exit Sub
    End If
    gAbcState = BAR_NOTE
    ' Only one bar field in a row
    If gAbcState > gAbcPrevState Then
        GoSub Init
        GoSub MainLoop
    ElseIf gAbcState = gAbcPrevState Then
        gAbcState = END_NOTE
    End If
    Exit Sub

'********************************************
'********************************************

Init:
    Return

'********************************************

MainLoop:
    GoSub EditNote
    gAbc(pVoice).bBarNote = True
    gAbc(pVoice).MeasNo = _
        gAbc(pVoice).MeasNo + 1
    Return

'********************************************

EditNote:
    Select Case pTypeCh
        Case ABAR ' 41
            ' A single bar may be followed by zero
            ' or more bars, colons, close brackets
            ' or digits (at a repeat).
            Call GetAbcString(pVoice, pChar, _
                ABAR, SAMECHAR, A$, bErr) ' 41
            If Len(A$) > 1 Then GoTo Select1
            'GoTo Select1 ' 19sep08
            Call GetAbcString(pVoice, pChar, _
                ACOLON, SAMECHAR, A$, bErr) ' 42
            If Len(A$) > 1 Then GoTo Select1
            Call GetAbcString(pVoice, pChar, _
                CBRACKET, SAMECHAR, A$, bErr) ' 5
            If Len(A$) > 1 Then GoTo Select1
            Call GetAbcString(pVoice, pChar, _
                ADIGIT, SAMETYPE, A$, bErr) ' 21
        Case ACOLON ' 42
            ' A single bar may be followed by zero
            ' or more bars, colons, close brackets
            ' or digits (at a repeat).
            Call GetAbcString(pVoice, pChar, _
                ABAR, SAMECHAR, A$, bErr) ' 41
            If Len(A$) > 1 Then GoTo Select1
            Call GetAbcString(pVoice, pChar, _
                ACOLON, SAMECHAR, A$, bErr) ' 42
        Case OBRACKET ' 3
            ' A single open bracker may be followed
            ' by zero or one (more) bars.
            Call GetAbcString(pVoice, pChar, _
                ABAR, SAMECHAR, A$, bErr) ' 41
        Case Else
    End Select
Select1:
    gAbc(pVoice).Name.Abc = A$
    GoSub DefaultTime
    GoSub AbcNoteTo4Part
    'call AbcNoteTo4Part(pVoice, bErr)
    GoSub GetLyricForThisNote
    Return

'********************************************
' In case a note name has no length following.
' E.g. if L: 1/8 @WN=32U, then:
DefaultTime:
    gAbc(pVoice).Time.Abc = ""
    gAbc(pVoice).Time.FourPart = A$
    gAbc(pVoice).Time.Len = 0
    Return

'********************************************

AbcNoteTo4Part:
    gAbc(pVoice).Name.FourPart = _
        gAbc(pVoice).Name.Abc
    Return

'********************************************

' NOT USED 19sep08

GetLyricForThisNote9:
      With gAbcInputLyric(pVoice)
        If .bUsed Then
            If gAbc(pVoice).Lyric(1) = "" Then
                  'gAbc(pVoice).lyric(1) = "|" ' 19sep08
            End If
        End If
      End With
      Return

'********************************************

' Initialized .Pos = 1 in sub InputLines at
' StoreSong1:
' After each use, .Pos points to the first non-
' blank after a string of one or more blanks.
' At the end, .Pos >=
'      len(gAbcInputLyric(pVoice).string)
' There is a blank after the last lyric word or bar.

GetLyricForThisNote:
      With gAbcInputLyric(pVoice)
        'GoSub Test1
        If .bUsed Then
            If .Pos < Len(.String) Then
              Loc1 = InStr(Mid$(.String, .Pos), " ")
              If Loc1 > 0 Then
                  C$ = Mid$(.String, .Pos, Loc1 - 1)
                  If C$ <> "|" Then
                      gAbc(pVoice).Lyric(1) = "|"
                  Else
                        gAbc(pVoice).Lyric(1) = C$
                        .Pos = .Pos + Loc1 - 1 ' blank
                        GoSub GetLyricNextNonBlank
                  End If
              Else
                  gAbc(pVoice).Lyric(1) = ""
                  I = I
                  Stop
              End If
            End If
        End If
      End With
    Return

'********************************************

' Point .Pos to the next non-blank in .String.

GetLyricNextNonBlank:
      With gAbcInputLyric(pVoice)
            'GoSub Test1
            D$ = Mid$(.String, .Pos, 1)
            Do While D$ = " " And .Pos < Len(.String)
                  .Pos = .Pos + 1
                  D$ = Mid$(.String, .Pos, 1)
            Loop
            If .Pos > Len(.String) Then
                  Stop ' insurance
            End If
      End With
      Return

'********************************************

Test1:
       If pVoice = 3 Then
           If gAbcInputLyric(pVoice).Pos > 1422 Then
               I = I
           End If
       End If
       Return
       
End Sub



Private Sub ParseAbcNoteTime(pVoice As Integer, _
                        pChar As String, _
                        pTypeCh As Integer, _
                        pbErr As Boolean)
    ' called by sub ParseAbcNote
    ' calls sub GetAbcString
    ' (Calls sub DisplayText(E$))
    ' Globals used:
    ' Globals set:

    ' If entered with pTypeCh=digit or slash or broken
    ' ABC time type,
    ' then get the next ABC time field,
    ' e.g. n or / or > or <, and store in
    '   gAbc(pVoice).Time.Abc = A$
    '   gAbc(pVoice).Time.Len
    ' Then convert it to 4Part format,
    ' e.g. E3/2 and store in
    '   gAbc(pVoice).Time.FourPart

    Dim I As Integer
    Dim bErr As Boolean
    Dim A$
    Dim B$
    Dim C$
    Dim Flag As Integer
    Dim Numer As Integer
    Dim Denom As Integer
    Const EMPTY1 As String = ""
    Dim E$
    Dim Loc1 As Integer

'********************************************
'********************************************

Main:
    gAbcState = NOTE_TIME
    ' Only one time field per note
    If gAbcState > gAbcPrevState Then
        GoSub Init
        GoSub MainLoop
    ElseIf gAbcState = gAbcPrevState Then
        gAbcState = END_NOTE
    End If
    Exit Sub

'********************************************
'********************************************

Init:
    Return

'********************************************

MainLoop:
    GoSub EditTime
    Return

'********************************************

EditTime:
    ' Only one of these 3 cases should occur
    ' for a note.  If more then one, the last
    ' one takes effect, without warning msg.
    Select Case pTypeCh
        Case ADIGIT ' 21 d,dd,d/d,dd/dd, etc
            Call GetAbcString(pVoice, pChar, _
                ADIGIT, SAMETYPE, A$, bErr)
            gAbc(pVoice).Time.Abc = A$
            Numer = Val(A$)
            Denom = 1 ' insurance
            If fcnGetAbcCh(pVoice, pChar) = "/" _
            Then ' call
                gAbc(pVoice).Time.Abc = A$ & pChar
                GoSub GetDigits
            Else
                Call PushAbcCh(pVoice, pChar, bErr)
            End If
            gAbc(pVoice).Time.Len = Numer * _
                gSong(gSongLast).L.LUnits / Denom
            GoSub Get4PartTime
        Case ASLASH ' 22 /,//,///... or /d,/dd,/ddd...
            Call GetAbcString(pVoice, pChar, _
                ASLASH, SAMECHAR, A$, bErr)
            gAbc(pVoice).Time.Abc = "1" & A$
            Numer = 1
            If A$ = "/" Then
                GoSub GetDigits
            Else
                Select Case A$
                    Case "//"
                        Denom = 4
                    Case "///"
                        Denom = 8
                    Case "////"
                        Denom = 16
                    Case "/////"
                        Denom = 32
                    Case Else
                        Stop
                End Select
            End If
            gAbc(pVoice).Time.Len = Numer * _
                gSong(gSongLast).L.LUnits / Denom
            ' TODO 12/18/6
            GoSub GetFraction
            GoSub Get4PartTime
        Case ABROKEN ' 23
            ' BROKEN rhythm's <<< or > may
            ' follow a note name e.g. A>B
            Call GetAbcString(pVoice, pChar, _
                pTypeCh, SAMECHAR, A$, bErr)
            ' TODO length check?
            gAbc(pVoice).Time.Abc = A$
            ' TODO 12/14/6 convert to 4Part?
            GoSub Get4PartTime
        Case Else
    End Select
    Return

'********************************************

     ' A fraction like 1/8 is an "E" or an eighth.
GetFraction:
     B$ = Format(gSong(gSongLast).L.Denom * Denom)
     gAbc(pVoice).Time.Abc = Format(Numer) & "/" & B$
     Return
     
'********************************************

GetDigits:
    Call GetAbcString(pVoice, EMPTY1, _
        ADIGIT, SAMETYPE, C$, bErr)
    If C$ = "" Then
        If A$ = "/" Then
            C$ = "2"
        Else
            C$ = "1"
        End If
    End If
    gAbc(pVoice).Time.Abc = _
        gAbc(pVoice).Time.Abc & C$
    Denom = Val(C$)
    Return

'********************************************

    ' If .Time.Abc is one of the standard lengths
    ' like 1/2, 1/4, etc, then use the proper
    ' letter for .Time.FourPart.
    ' Eg. if Time.Abc="1/8", then .Time.FourPart="E",
    ' Else use: .Time.FourPart="E" & .Abc eg. "E3/2"
    ' TODO 12/17/6
    '   Why use "E" as the default in "E3/2" ?
Get4PartTime:
     Call ParseAbc4PartTime( _
               gAbc(pVoice).Time.Abc, _
               gAbc(pVoice).Time.FourPart)
    Return
    
     ' TODO 12/18/6 not used
    I = InStr(DEFAULTTIMES, _
        " " & gAbc(pVoice).Time.Abc & " ")
    If I > 0 Then
        gAbc(pVoice).Time.FourPart = _
            Mid$(DEFAULTTIMES, I - 1, 1)
    Else
        gAbc(pVoice).Time.FourPart = _
            gSong(gSongLast).L.Default & _
            gAbc(pVoice).Time.Abc ' eg. e3/2
    End If
    ' Use lowercase for the 4Part time letter names
    ' for better visibility.
    gAbc(pVoice).Time.FourPart = _
        LCase$(gAbc(pVoice).Time.FourPart)
    Return

End Sub


Private Sub ParseAbc4PartTime(pAbc As String, _
                         pFourPart As String)

    ' called by sub ParseAbc
    ' called by Sub ParseAbcNoteTime
    
    ' Globals used:
    ' Globals set:

    ' Enter with a fraction like 1/8 in
    '   pAbc
    ' and then convert it to 4Part format
    ' name like S
    ' or append 3/2 to l.Default (usually "E")
    ' e.g. E3/2 and store in
    '   pFourPart
    ' in lowercase.

    Dim I As Integer
    Dim E$

'********************************************
'********************************************

Main:
     GoSub Init
     GoSub MainLoop
    Exit Sub

'********************************************
'********************************************

Init:
    Return

'********************************************

MainLoop:
    GoSub Get4PartTime
    Return

'********************************************

    ' If .Time.pAbc is one of the standard lengths
    ' like 1/2, 1/4, etc, then use the proper
    ' letter for .Time.pFourPart.
    ' Eg. if Time.Abc="1/8", then .Time.FourPart="E",
    ' Else use: .Time.FourPart="E" & .pAbc eg. "E3/2"
    ' TODO 12/17/6
    '   Why use "E" as the default in "E3/2" ?
Get4PartTime:
    I = InStr(DEFAULTTIMES, _
        " " & pAbc & " ")
    If I > 0 Then
        pFourPart = _
            Mid$(DEFAULTTIMES, I - 1, 1)
    Else
        pFourPart = _
            gSong(gSongLast).L.Default & _
            pAbc ' eg. e3/2
    End If
    ' Use lowercase for the 4Part time letter names
    ' for better visibility.
    pFourPart = LCase$(pFourPart)
    Return

End Sub


Private Sub GetAbcString(pVoice As Integer, _
                pChar As String, _
                pTypeCh As Integer, _
                pFlag As Integer, _
                pString As String, _
                pbErr As Boolean)
    ' called by sub ParseAbcNote
    ' calls function fcnGetAbcCh
    ' calls sub PushAbcCh

    ' Globals used:
    ' Globals set:

    ' Return a string of chars in pString,
    ' starting with pChar.
    ' pFlag controls what chars to return
    ' in the string.  Keep getting chars
    ' that are the same char, or same type,
    ' or until the same char, or until the
    ' same type is reached again.

    Dim I As Integer
    Dim bErr As Boolean
    Dim B$
    Dim E$

'********************************************
'********************************************

Main:
    GoSub Init
    'Debug.Print
    GoSub MainLine
    'GoSub Debug1
    'Debug.Print
    Exit Sub

'********************************************
'********************************************

Init:
    Return

'********************************************

MainLine:
    Select Case pFlag
        Case SAMECHAR
            GoSub GetSameChar
        Case SAMETYPE
            GoSub GetSameType
        Case TILLCHAR
            GoSub GetTillSameChar
        Case TILLTYPE
            GoSub GetTillSameType
        Case Else
            GoSub Err1
            I = I ' testing
            Stop
    End Select
    Return

'********************************************

GetSameChar:
    pString = pChar
    While fcnGetAbcCh(pVoice, B$) <> EOS _
          And B$ = pChar
          pString = pString & B$
    Wend
    GoSub Debug1
    If B$ <> EOS Then
        Call PushAbcCh(pVoice, B$, bErr)
    End If
    Return

'********************************************

GetSameType:
    pString = pChar
    While fcnGetAbcCh(pVoice, B$) <> EOS _
          And pTypeCh = gxAbcTable(Asc(B$)) ' call
          pString = pString & B$
    Wend
    GoSub Debug1
    If B$ <> EOS Then
        Call PushAbcCh(pVoice, B$, bErr)
    End If
    Return

'********************************************

GetTillSameChar:
    pString = pChar
    Do While fcnGetAbcCh(pVoice, B$) <> EOS
        pString = pString & B$
        If B$ = pChar Then
            Exit Do
        End If
    Loop
    GoSub Debug1
    If B$ = EOS Then ' keep on going
        GoSub Err2
        pString = pString & pChar
    End If
    Return

'********************************************

GetTillSameType:
    pString = pChar
    Do While fcnGetAbcCh(pVoice, B$) <> EOS
        pString = pString & B$
        If pTypeCh = gxAbcTable(Asc(B$)) Then
            Exit Do
        End If
    Loop
    GoSub Debug1
    If B$ = EOS Then ' keep on going
        GoSub Err2
        pString = pString & pChar
    End If
    Return

'********************************************

Debug1:
    Return
    Debug.Print "GetAbcString: pchar, pvoice, pstring="; _
        pChar; pVoice; "/"; pString; "/"
    Return

'********************************************

Err1:
    gInputErrs = gInputErrs + 1
    E$ = "GetAbcString: invalid pFlag=" & _
        pFlag & _
        " for Voice=" & pVoice & _
        "in song=" & gSongLast & _
        "at lineno=" & gInputLineNo & _
        "in ABC line=" & gTextLine
    Call DisplayText(E$)
    Debug.Print E$
    Return

'********************************************

Err2:
    gInputErrs = gInputErrs + 1
    E$ = "GetAbcString: missing matching char=" & _
        pChar & _
        " for Voice=" & pVoice & _
        "in song=" & gSongLast & _
        "at lineno=" & gInputLineNo & _
        "in ABC line=" & gTextLine
    Call DisplayText(E$)
    Debug.Print E$
    Return

End Sub




Private Sub InitAbcTable()
    ' called by sub Init4Part
        
    ' gxAbcTable() is used in sub ParseAbcNote
    
    ' Globals used: gDblNames()=, gDblDescs()=,
    '       gChord()..., gNoteName()...
    ' Globals set:

    Dim I As Integer

'********************************************
'********************************************

Main:
    GoSub Init
    GoSub MainLine
    Exit Sub

'********************************************
'********************************************

Init:
    For I = 0 To 255
        gxAbcTable(I) = 0
    Next I
    Return

'********************************************

' Note you cannot store two different values
' e.g. CBRACKET=5 and ARBRACKET=43 at the
' same ASC offset Asc("[") in the table, that is,
'       gxAbcTable(Asc("["))
MainLine:
' Pre-Note Opens:
    gxAbcTable(Asc("(")) = OPAREN ' 1
   'gxAbcTable(Asc("(")) = OSLUR ' 1
    gxAbcTable(Asc("{")) = OBRACE ' 2
    gxAbcTable(Asc("[")) = OBRACKET ' 3

' Pre-Note Closes:
    gxAbcTable(Asc("}")) = CBRACE ' 4
    gxAbcTable(Asc("]")) = CBRACKET ' 5

' Pre-Note:
    gxAbcTable(34) = ADQUOTE ' 7
    gxAbcTable(Asc(".")) = ASTACCATO ' 8
    For I = Asc("H") To Asc("Y") '
        gxAbcTable(I) = ALETTER ' 9
    Next I
    For I = Asc("h") To Asc("w")
        gxAbcTable(I) = ALETTER ' 9
    Next I

' In-Note:
    gxAbcTable(Asc("_")) = ACCIDEN ' 11
    gxAbcTable(Asc("=")) = ACCIDEN ' 11
    gxAbcTable(Asc("^")) = ACCIDEN ' 11
    For I = Asc("A") To Asc("G")
        gxAbcTable(I) = ANOTE ' 12
    Next I
    For I = Asc("a") To Asc("g")
        gxAbcTable(I) = ANOTE ' 12
    Next I
    gxAbcTable(Asc("x")) = ANOTE ' 12 invisible Rest
    gxAbcTable(Asc("z")) = ANOTE ' 12 Rest
    gxAbcTable(Asc(",")) = ACOMMA ' 13 lower octave
    gxAbcTable(Asc("'")) = AQUOTE ' 13 higher octave

' Time:
    For I = Asc("0") To Asc("9")
        gxAbcTable(I) = ADIGIT ' 21
    Next I
    gxAbcTable(Asc("/")) = ASLASH ' 22
    gxAbcTable(Asc(">")) = AGT ' 23 = ABROKEN
    gxAbcTable(Asc("<")) = ALT ' 23

' Post-Note:
    gxAbcTable(Asc("-")) = ATIE ' 31
    gxAbcTable(Asc("y")) = ASPACE ' 32
    gxAbcTable(Asc(")")) = CPAREN ' 33
   'gxAbcTable(Asc(")")) = CSLUR ' 33
    gxAbcTable(Asc(" ")) = ABLANK ' 34

' Bar-Chars:
    gxAbcTable(Asc("|")) = ABAR ' 41
    gxAbcTable(Asc(":")) = ACOLON ' 42
' Bar-Chars preceded by:
    'gxAbcTable(Asc("[")) = OBRACKET ' 3
    'gxAbcTable(Asc(".")) = ASTACCATO ' 8
' Bar-Chars followed by:
    'gxAbcTable(Asc("]")) = CBRACKET ' 5
    'gxAbcTable(I) = ADIGIT ' 21
    'gxAbcTable(Asc("/")) = ASLASH ' 22

' Modifier Char:
' (* -* OR z* extends the ( - or z to all voices
    gxAbcTable(Asc("*")) = ASTERISK ' 43

' END Flag Chars:
    gxAbcTable(&HFE) = AEOS ' 51
    gxAbcTable(&HFF) = AEOF ' 52

    Return

End Sub




Private Sub WriteAllChords()
    ' called by sub cmdWriteChord_Click

    ' calls sub InitChordsForKey
    ' calls sub MainChordsForKey
    ' calls sub WriteDblTypes
    ' calls cmdHarmony_Click
    
    ' Write out all a file for each key in this
    ' gKeyMode.  Each file contains all the valid
    ' chords for each scale step in the key.
    
    ' These files are for documentation and for
    ' trouble-shooting.  The files are not used in
    ' processing by the 4Part program, but the same
    ' data is used in internal tables.
    
    ' cmdWriteAll_click changes the boolean value
    ' of gbWriteAll.
    ' If false,
    '   sub WriteDblTypes
    ' writes only one file for the gKeyName and
    ' gKeyMode of the current song.
    ' If true,
    '   Sub WriteAllChords
    ' writes a file for each gKeyName in this gKeyMode.

    ' Do not clobber the chords in the table for the
    ' current gSongCurr.  Instead save gChordLast
    ' and restore it after all is done.
    ' Do the same with gKeyModeSave and gKeyNameSave
    ' as insurance.

    Dim I As Integer
    Dim KeyModeSave As Integer
    Dim KeyNameSave As String
    Dim MajorKey As Variant
    Dim MinorKey As Variant
    Dim DorianKey As Variant
    Dim PhrygianKey As Variant
    Dim LydianKey As Variant
    Dim MixolydianKey As Variant

    ' See sub InitHalfStepNames for the details
    ' on these arrays taken from it.
    MajorKey = Array( _
        "Cb", "Gb", "Db", "Ab", "Eb", "Bb", "F", _
        "C", "G", "D", "A", "E", "B", "F#", "C#")
    MinorKey = Array( _
        "Ab", "Eb", "Bb", "F", "C", "G", "D", _
        "A", "E", "B", "F#", "C#", "G#", "D#", "A#")
    DorianKey = Array _
        ("Db", "Ab", "Eb", "Bb", "F", "C", "G", _
        "D", "A", "E", "B", "F#", "C#", "G#", "D#")
    PhrygianKey = Array _
        ("Eb", "Bb", "F", "C", "G", "D", "A", _
        "E", "B", "F#", "C#", "G#", "D#", "A#", "E#")
    LydianKey = Array _
        ("Fb", "Cb", "Gb", "Db", "Ab", "Eb", "Bb", _
        "F", "C", "G", "D", "A", "E", "B", "F#")
    MixolydianKey = Array _
        ("Gb", "Db", "Ab", "Eb", "Bb", "F", "C", _
        "G", "D", "A", "E", "B", "F#", "C#", "G#")

    gChordLastSave = gChordLast
    KeyModeSave = gKeyMode
    KeyNameSave = gKeyName
    gGrandTotChordsValid = 0
    With gSong(gSongCurr)
        gKeyMode = .Key(1).Mode
        Select Case gKeyMode
        Case MAJOR
            gKeyMode = MAJOR
            For I = 0 To 14
                gChordLast = gChordLastSave
                gKeyName = MajorKey(I)
                Call InitChordsForKey
                Call MainChordsForKey
                Call WriteDblTypes
            Next I
            GoSub Total1
        Case MINORNAT, MINORHARM, MINORMELUP
            gKeyMode = MINORNAT
            For I = 0 To 14
                gChordLast = gChordLastSave
                gKeyName = MinorKey(I)
                Call InitChordsForKey
                Call MainChordsForKey
                Call WriteDblTypes
            Next I
            GoSub Total1
        Case DORIAN
            gKeyMode = DORIAN
            For I = 0 To 14
                gChordLast = gChordLastSave
                gKeyName = DorianKey(I)
                Call InitChordsForKey
                Call MainChordsForKey
                Call WriteDblTypes
            Next I
            GoSub Total1
        Case PHRYGIAN
            gKeyMode = PHRYGIAN
            For I = 0 To 14
                gChordLast = gChordLastSave
                gKeyName = PhrygianKey(I)
                Call InitChordsForKey
                Call MainChordsForKey
                Call WriteDblTypes
            Next I
            GoSub Total1
        Case LYDIAN
            gKeyMode = LYDIAN
            For I = 0 To 14
                gChordLast = gChordLastSave
                gKeyName = LydianKey(I)
                Call InitChordsForKey
                Call MainChordsForKey
                Call WriteDblTypes
            Next I
            GoSub Total1
        Case MIXOLYDIAN
            gKeyMode = MIXOLYDIAN
            For I = 0 To 14
                gChordLast = gChordLastSave
                gKeyName = MixolydianKey(I)
                Call InitChordsForKey
                Call MainChordsForKey
                Call WriteDblTypes
            Next I
            GoSub Total1
        Case Else
            Debug.Print "WriteAllChords: "; _
                "KEY must be MAJOR, MINORNAT,"; _
                "MINORHARM, MINORMELUP, DORIAN, "; _
                "PHRYGIAN, LYDIAN or MIXOLYDIAN. "
        End Select
    End With
    gChordLast = gChordLastSave
    gChordLastSave = 0 ' needed for sub WriteAllChords
    gKeyMode = KeyModeSave
    gKeyName = KeyNameSave
    Call cmdHarmony_Click ' reset current song
    Exit Sub
Total1:
    Debug.Print "gGrandTotChordsValid="; _
        gGrandTotChordsValid
Return
End Sub


Private Sub InitChordsForKey()

    ' called by sub cmdChords_Click
    ' called by sub StoreNotes
    ' called by Sub WriteAllChords

    ' calls sub InitHalfStepNames
    ' calls InitKey ' after InitHalfStepNames
    ' calls InitVoiceRange ' before fcnToIx
    ' calls function fcnToIx
    ' (Calls sub InitDoubleNames - call only once)
    ' calls sub InitNoteNames ' [gHalfStepName()
    ' calls sub InitPrimaryTones ' [gKeyIndex]
    ' calls sub InitNeverDouble ' [gKeyIndex, gKeyMode]
    ' calls sub InitScaleDegree ' [gKeyIndex]

    ' Globals used: gKeyName,
    ' Globals set: gKeyNote =, gKeyIndex =

    Dim I As Integer

    'gbDebugPrint = False ' don't print debug lines
    'gbDebugPrint = True ' Print debug lines

    'Begin - Initialize values for next chord table
    gDoubled = 0
    gDoubledIndex = 0
    'gChordLast = 0
    gTotChordsChecked = 0
    gTotChordsValid = 0
    gTotValidType = 0
    For I = 0 To DBLTYPEMAX ' 18
        gDblType(I).TotVal = 0
    Next I
    For I = 1 To 7
        gNumOfTriadNotes(I) = 0
        gTriadNoteIndex(I) = 0
    Next I
    ' End - Initialize values for next chord table

    ' gKeyName is set in sub cmdChords_Click.
    ' TODO 12/10/6. Bypass if K2: or K3: or K4:
    'If gSongKey = 1 Then
        Call InitHalfStepNames ' [gKeyName] before fcnToIx
    'End If
    
    'call InitScales - not needed
    'call InitDoubleNames ' call only once
    Call InitNoteNames ' gHalfStepName()
    '   was set in InitHalfStepNames.
    ' TODO 11/3/6
    Call InitKey ' after InitHalfStepNames 23jul08
    Call InitVoiceRange ' before fcnToIx
    
    gKeyNote = gKeyName & "0"
    If gKeyNote = "A0" Then
        I = I
    End If
    ' Use function fcnToIx to return the index of
    ' the note, e.g. G#0, in gNoteName.
    gKeyIndex = 0 ' for the first time called
    ' by sub InitChordsForKey
    gKeyIndex = fcnToIx(gKeyNote) ' call
    If gKeyIndex = 99 Then
        GoTo Err1
    End If
    'If gKeyIndex < -1 Then Stop ' insurance TODO

    'If Not gbEffortSetup Then
    '    call InitVoiceEffort - not needed
    'End If
    Call InitPrimaryTones ' [gKeyIndex]
    ' gKeyMode is set in sub cmdChords_Click.
    ' TODO CHECK THIS FOR MINOR KEYS:
    Call InitNeverDouble ' [gKeyIndex, gKeyMode]
    Call InitNaturals ' [gKeyIndex, gKeyMode] must
        ' follow call InitNeverDouble 16jul08
    Call InitScaleDegree ' [gKeyIndex]
    Exit Sub

Err1:
    gInputErrs = gInputErrs + 1
    Debug.Print "InitChordsForKey: invalid gKeyNote="; _
        gKeyNote; " in song no="; gSongLast
    Stop

End Sub

Private Function fcnToIx(pNote As String) As Integer
    
    
    ' (Called by sub InitVoiceRange)
    ' called by sub StoreVoiceVn
    ' called by sub InitChordsForKey
    ' (Called by sub StoreOptionRange)

    ' ?
    ' called by sub ScanTriads

    ' Enter with a string name pNote like "G1" or
    ' "G#4" as an input note from the scale, or
    ' in Minor, with an altered note of the form
    ' #C4 or from FB: #3 or FB: #^6.
    ' Check for valid format.  Search for that
    ' note name in gNoteName, and
    ' use the fcnToIx function to convert
    ' the name to its half-step index in gNoteName
    ' or gTriads.
    Dim I As Integer
    Dim K As Integer
    Dim R As Integer
    Dim Index As Integer
    Dim KeyName$
    Dim Letter1$
    Dim Middle$
    Dim Digit$
    Dim bErr As Boolean

'*********************************************
'*********************************************
Main:
    GoSub Init
    If Not bErr Then
        GoSub MainLine
    End If
    Exit Function

'*********************************************
'*********************************************

Init:
    fcnToIx = 99 ' error return
    pNote = Trim$(pNote)
    If Len(pNote) > 3 Or Len(pNote) < 2 Then
        GoSub InvalidNote
    End If
    If pNote = "A0" And gSongCurr = 8 Then
        I = I
    End If
    
    Return
    
'*********************************************

MainLine:
    Digit$ = Right$(pNote, 1)
    If Digit$ < "0" Or Digit$ > "8" Then
        GoSub InvalidNote
        GoTo MainReturn
    Else
        R = Asc(Digit$) - Asc("0")
    End If

    'KeyName$ = UCase$(pNote) ' eg can't uppercase Gb3
    ' NOTE that "Cb3" = "B 3" as the same offset
    '   in gnoteName.
    ' gKeyIndex = 0 on the first call from
    ' TODO 11/18/6
    'K = gKeyIndex + 12 * R
    ' TODO 11/18/6
    K = 12 * R
    If K > 0 Then
        K = K - 1 ' eg for Cb1  14aug08
    End If
    Index = -1
    For I = 0 To 11 + 2 ' +2 for #B3 in C# m. 14aug08
        If pNote = gNoteName(K + I) Then
            Index = K + I
            Exit For
        End If
    Next I
    
    If Index = -1 Then
        If pNote <> "A0" Then ' See sub InitTriads
            GoSub Err1
        End If
    ElseIf Index > NOTESMAX Then
        GoSub ErrHighNote
    ' TODO check this:
    ElseIf Index < TRIADSMIN Then
        GoSub ErrLowNote
    Else
        fcnToIx = Index
    End If
MainReturn:
    Return

'*********************************************

' If fcnToIx returns 99, then4Part should always do
' a retry.  If the retry fails, then print err msgs.
Err1:
    bErr = True
    If gbfcnToIxErrMsg Then ' set in StoreVoiceVn
        'gInputErrs = gInputErrs + 1 ' 27jun08
        Debug.Print "fcnToIx: note='"; pNote; _
            "' in song="; gSongLast; _
            "missing from gNoteName."; _
            " Retry." ' 27jun08
        Debug.Print " gNoteName(K + I)=";
        For I = 0 To 11
            Debug.Print K + I; gNoteName(K + I);
        Next I
        Debug.Print
        I = I
        'Stop
    End If
    Return
        'Debug.Print " First try."

'*********************************************

InvalidNote:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "fcnToIx:E1 invalid note='"; pNote; _
        "' in song=";
    If gSongCurr = 0 Then
        Debug.Print gSongLast
    Else
        Debug.Print gSongCurr
    End If
    Return

'*********************************************

ErrHighNote:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "fcnToIx:E2 invalid note='"; pNote; _
        "' is above C8 in song=";
    If gSongCurr = 0 Then
        Debug.Print gSongLast
    Else
        Debug.Print gSongCurr
    End If
    Return

'*********************************************

ErrLowNote:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "fcnToIx:E3 invalid note='"; pNote; _
        "' is below A0 in song=";
    If gSongCurr = 0 Then
        Debug.Print gSongLast
    Else
        Debug.Print gSongCurr
    End If
    Return

End Function

Private Function fcnToIxCMajor(pNote As String) As Integer
    ' called by sub InitVoiceRange

    ' This sub fcnToIxCMajor is like
    ' sub fcnToIx, but it is meant to be
    ' used for the notes listed in
    '   OP: MISC RANGE F2 G3
    ' which is specified in C Major, no matter
    ' what the key of the current song is.

    ' Enter with a string name pNote like "G1" or
    ' "G#4" as an input note from the scale, or
    ' in Minor, with an altered note of the form
    ' #C4 or from FB: #3 or FB: #^6.
    ' Check for valid format.  Search for that
    ' note name in gNoteNameCMajor, and
    ' use the fcnToIxCMajor function to convert
    ' the name to its half-step index in gNoteNameCMajor
    ' or gTriads.
    Dim I As Integer
    Dim K As Integer
    Dim R As Integer
    Dim Index As Integer
    Dim KeyName$
    Dim Letter1$
    Dim Middle$
    Dim Digit$
    Dim bErr As Boolean

'*********************************************
'*********************************************
Main:
    GoSub Init
    If Not bErr Then
        GoSub MainLine
    End If
    Exit Function

'*********************************************
'*********************************************

Init:
    fcnToIxCMajor = 99 ' error return
    pNote = Trim$(pNote)
    If Len(pNote) > 3 Or Len(pNote) < 2 Then
        GoSub InvalidNote
    End If
    Return
    
'*********************************************

MainLine:
    Digit$ = Right$(pNote, 1)
    If Digit$ < "0" Or Digit$ > "8" Then
        GoSub InvalidNote
        GoTo MainReturn
    Else
        R = Asc(Digit$) - Asc("0")
    End If

    'KeyName$ = UCase$(pNote) ' eg can't uppercase Gb3
    ' NOTE that "Cb3" = "B 3" as the same offset
    '   in gNoteNameCMajor.
    K = 12 * R
    Index = -1
    For I = 0 To 11
        If pNote = gNoteNameCMajor(K + I) Then
            Index = K + I
            Exit For
        End If
    Next I
    
    If Index = -1 Then
        GoSub Err1
    ElseIf Index > NOTESMAX Then
        GoSub ErrHighNote
    ' TODO check this:
    ElseIf Index < TRIADSMIN Then
        GoSub ErrLowNote
    Else
        fcnToIxCMajor = Index
    End If
MainReturn:
    Return

'*********************************************

Err1:
    bErr = True
    gInputErrs = gInputErrs + 1
    ' "' in song="; gSongCurr;
    Debug.Print "fcnToIxCMajor: note='"; pNote; _
        "' in song="; gSongLast; _
        "missing from HalfSteps"
    Debug.Print " gHalfStepName()=";
    For I = 0 To 11
        Debug.Print gHalfStepName(I);
    Next I
    Debug.Print
    Debug.Print " gHalfStepName0()=";
    For I = 0 To 11
        Debug.Print gHalfStepName0(I);
    Next I
    Debug.Print
    Return

'*********************************************

InvalidNote:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "fcnToIxCMajor: invalid note='"; pNote; _
        "' in song=";
    If gSongCurr = 0 Then
        Debug.Print gSongLast
    Else
        Debug.Print gSongCurr
    End If
    Return

'*********************************************

ErrHighNote:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "fcnToIxCMajor: invalid note='"; pNote; _
        "' is above C8 in song=";
    If gSongCurr = 0 Then
        Debug.Print gSongLast
    Else
        Debug.Print gSongCurr
    End If
    Return

'*********************************************

ErrLowNote:
    bErr = True
    gInputErrs = gInputErrs + 1
    Debug.Print "fcnToIxCMajor: invalid note='"; pNote; _
        "' is below A0 in song=";
    If gSongCurr = 0 Then
        Debug.Print gSongLast
    Else
        Debug.Print gSongCurr
    End If
    Return

End Function



Private Sub InitScales()
    ' called by sub Init4Part
    ' (Called by sub StoreNotes  - not needed)
    ' (Called by sub InitChordsForKey - not needed)
    ' calls

    ' Initialize the Roman numeral names, qualities,
    ' and "my half-step" numbers (0-11) corresponding
    ' to scale step numbers (^1-^7) of the 7
    ' diatonic triads of a key.
    Dim I As Integer

     ' Variants initialized by an array.
    Dim RomNamesMajor
    Dim RomNamesMinorNat
    Dim RomNamesMinorHarm
    Dim RomNamesMinorMelUp
    Dim RomNamesMinorMelDn
    ' Ottman Appendix D Medieval Modes p. 433.
    Dim RomNamesDorian ' like natural minor
    ' with a raised sixth scale step
    Dim RomNamesPhrygian ' like natural minor
    ' with a lowered second scale step
    Dim RomNamesLydian ' like major
    ' with a raised fourth scale step
    Dim RomNamesMixolydian ' like major
    ' with a lowered seventh scale step
    'Dim RomNamesAeolian ' like natural minor
    'Dim RomNamesIonian ' like natural major

    Dim RomHalfStepsMajor As Variant
    Dim RomHalfStepsMinorNat As Variant
    Dim RomHalfStepsMinorHarm As Variant
    Dim RomHalfStepsMinorMelUp As Variant
    Dim RomHalfStepsMinorMelDn As Variant
    Dim RomHalfStepsDorian As Variant
    Dim RomHalfStepsPhrygian As Variant
    Dim RomHalfStepsLydian As Variant
    Dim RomHalfStepsMixolydian As Variant

    Dim RomQualMajor As Variant
    Dim RomQualMinorNat As Variant
    Dim RomQualMinorHarm As Variant
    Dim RomQualMinorMelUp As Variant
    Dim RomQualMinorMelDn As Variant
    Dim RomQualDorian As Variant
    Dim RomQualPhrygian As Variant
    Dim RomQualLydian As Variant
    Dim RomQualMixolydian As Variant

    ' Ottman p.29
    RomNamesMajor = Array("", "I", "ii", _
        "iii", "IV", "V", "vi", "vii o", "I")
    RomNamesMinorNat = Array("", "i", "ii o", _
        "III", "iv", "v", "VI", "VII", "i")
    RomNamesMinorHarm = Array("", "i", "ii o", _
        "III+", "iv", "V", "VI", "vii o", "i")
    ' Melodic scale ascending
    RomNamesMinorMelUp = Array("", "i", "ii", _
        "III+", "IV", "V", "vi o", "vii o", "i")
    ' Melodic scale decending = Natural minor scale
    RomNamesMinorMelDn = RomNamesMinorNat
    ' Dorian is like C Major starting on D.
    RomNamesDorian = Array("", "i", "ii", _
        "III", "IV", "v", "vi o", "VII", "i")
    ' Phrygian is like C Major starting on E.
    RomNamesPhrygian = Array("", "i", "II", _
        "III", "iv", "v o", "VI", "vii", "i")
    ' Lydian is like C Major starting on F.
    RomNamesLydian = Array("", "I", "II", _
        "iii", "iv o", "V", "vi", "vii", "I")
    ' Mixolydian is like C Major starting on G.
    RomNamesMixolydian = Array("", "I", "ii", _
        "iii o", "IV", "v", "vi", "VII", "I")

    RomQualMajor = Array(0, QUALMAJOR, QUALMINOR, _
                QUALMINOR, QUALMAJOR, QUALMAJOR, _
                QUALMINOR, QUALDIM, QUALMAJOR)
    RomQualMinorNat = Array(0, QUALMINOR, QUALDIM, _
                QUALMAJOR, QUALMINOR, QUALMINOR, _
                QUALMAJOR, QUALMAJOR, QUALMINOR)
    RomQualMinorHarm = Array(0, QUALMINOR, QUALDIM, _
                QUALAUGM, QUALMINOR, QUALMAJOR, _
                QUALMAJOR, QUALDIM, QUALMINOR)
    ' Melodic scale ascending
    RomQualMinorMelUp = Array(0, QUALMINOR, QUALMINOR, _
                QUALAUGM, QUALMAJOR, QUALMAJOR, _
                QUALDIM, QUALDIM, QUALMINOR)
    ' Melodic scale decending = Natural minor scale
    RomQualMinorMelDn = RomQualMinorNat
    ' Dorian is like C Major starting on D.
    RomQualDorian = Array(0, QUALMINOR, QUALMINOR, _
                QUALMAJOR, QUALMAJOR, QUALMINOR, _
                QUALDIM, QUALMAJOR, QUALMINOR)
    ' Phrygian is like C Major starting on E.
    RomQualPhrygian = Array(0, QUALMINOR, QUALMAJOR, _
                QUALMAJOR, QUALMINOR, QUALDIM, _
                QUALMAJOR, QUALMINOR, QUALMINOR)
    ' Lydian is like C Major starting on F.
    RomQualLydian = Array(0, QUALMAJOR, QUALMAJOR, _
                QUALMINOR, QUALDIM, QUALMAJOR, _
                QUALMINOR, QUALMINOR, QUALMAJOR)
    ' Mixolydian is like C Major starting on G.
    RomQualMixolydian = Array(0, QUALMAJOR, QUALMINOR, _
                QUALDIM, QUALMAJOR, QUALMINOR, _
                QUALMINOR, QUALMAJOR, QUALMAJOR)


' Ottman p. 9-11
' Diatonic scale steps:      ^1,^2,^3,^4,^5,^6,^7, ^8
' Major: half-steps between ^3-^4 and ^7-^8:
     RomHalfStepsMajor = Array(0, 0, 2, 4, 5, 7, 9, 11, 12)
'                                ^1 ^2 ^3 ^4 ^5 ^6  ^7  ^8

' Natural Minor: half-steps between ^2-^3, and ^5-^6:
' To change major to natural minor, lower ^3, ^6, ^7.
'    RomHalfStepsMajor = Array(0, 0, 2, 4, 5, 7, 9, 11, 12)
  RomHalfStepsMinorNat = Array(0, 0, 2, 3, 5, 7, 8, 10, 12)
'                                ^1 ^2 ^3 ^4 ^5 ^6  ^7  ^8
'                                       b        b   b

' Harmonic Minor: half-steps between ^2-^3, ^5-^6, #^7-^8.
' Note that ^6-#^7 is A2, augmented 2nd.
' To change major to harmonic minor, lower ^3, ^6.
'    RomHalfStepsMajor = Array(0, 0, 2, 4, 5, 7, 9, 11, 12)
 RomHalfStepsMinorHarm = Array(0, 0, 2, 3, 5, 7, 8, 11, 12)
'                                ^1 ^2 ^3 ^4 ^5 ^6  ^7  ^8
'                                       b        b

' Ascending Melodic Minor: half-steps between 2-3 and #7-8:
' To change major to ascending melodic minor, lower ^3.
'    RomHalfStepsMajor = Array(0, 0, 2, 4, 5, 7, 9, 11, 12)
RomHalfStepsMinorMelUp = Array(0, 0, 2, 3, 5, 7, 9, 11, 12)
'                                ^1 ^2 ^3 ^4 ^5 ^6  ^7  ^8
'                                       b

' Melodic scale decending = Natural minor scale
RomHalfStepsMinorMelDn = RomHalfStepsMinorNat

' Dorian mode is like natural minor with the sixth
' scale step ^6 raised.
' Dorian: half-steps between ^2-^3, and ^6-^7:
' To change natural minor to Dorian, raise ^6.
'RomHalfStepsMinorNat= Array(0, 0, 2, 3, 5, 7, 8, 10, 12)
  RomHalfStepsDorian = Array(0, 0, 2, 3, 5, 7, 9, 10, 12)
'                              ^1 ^2 ^3 ^4 ^5 ^6  ^7  ^8
'                                              #

' Phrygian mode is like natural minor with the second
' scale step ^2 lowered.
' Phrygian: half-steps between ^1-^2, and ^5-^6:
' To change natural minor to Dorian, lower ^2.
' RomHalfStepsMinorNat = Array(0, 0, 2, 3, 5, 7, 8, 10, 12)
  RomHalfStepsPhrygian = Array(0, 0, 1, 3, 5, 7, 8, 10, 12)
'                                ^1 ^2 ^3 ^4 ^5 ^6  ^7  ^8
'                                    b

' Lydian mode is like major with the fourth
' scale step ^4 raised.
' Lydian: half-steps between ^4-^5, and ^7-^8:
' To change major to Lydian, raise ^4.
'    RomHalfStepsMajor = Array(0, 0, 2, 4, 5, 7, 9, 11, 12)
    RomHalfStepsLydian = Array(0, 0, 2, 4, 6, 7, 9, 11, 12)
'                                ^1 ^2 ^3 ^4 ^5 ^6  ^7  ^8
'                                          #

' Mixolydian mode is like major with the seventh
' scale step ^7 lowered.
' Mixolydian: half-steps between ^2-^3, and ^6-^7:
' To change major to Mixolydian, lower ^7.
'    RomHalfStepsMajor = Array(0, 0, 2, 4, 5, 7, 9, 11, 12)
RomHalfStepsMixolydian = Array(0, 0, 2, 4, 6, 7, 9, 10, 12)
'                                ^1 ^2 ^3 ^4 ^5 ^6  ^7  ^8
'                                                    b


    For I = TONIC To LEADINGTONE
        gScale(MAJOR).RNumName(I) = RomNamesMajor(I)
        gScale(MAJOR).Qual(I) = RomQualMajor(I)
        gScale(MAJOR).HalfStep(I) = _
            RomHalfStepsMajor(I)
    Next I
    For I = TONIC To LEADINGTONE
        gScale(MINORNAT).RNumName(I) = RomNamesMinorNat(I)
        gScale(MINORNAT).Qual(I) = RomQualMinorNat(I)
        gScale(MINORNAT).HalfStep(I) = _
            RomHalfStepsMinorNat(I)
    Next I
    For I = TONIC To LEADINGTONE
        gScale(MINORHARM).RNumName(I) = RomNamesMinorHarm(I)
        gScale(MINORHARM).Qual(I) = RomQualMinorHarm(I)
        gScale(MINORHARM).HalfStep(I) = _
            RomHalfStepsMinorHarm(I)
    Next I
    For I = TONIC To LEADINGTONE
        gScale(MINORMELUP).RNumName(I) = RomNamesMinorMelUp(I)
        gScale(MINORMELUP).Qual(I) = RomQualMinorMelUp(I)
        gScale(MINORMELUP).HalfStep(I) = _
            RomHalfStepsMinorMelUp(I)
    Next I
    For I = TONIC To LEADINGTONE
        gScale(MINORMELDN).RNumName(I) = RomNamesMinorMelDn(I)
        gScale(MINORMELDN).Qual(I) = RomQualMinorMelDn(I)
        gScale(MINORMELDN).HalfStep(I) = _
            RomHalfStepsMinorMelDn(I)
    Next I
    For I = TONIC To LEADINGTONE
        gScale(DORIAN).RNumName(I) = RomNamesDorian(I)
        gScale(DORIAN).Qual(I) = RomQualDorian(I)
        gScale(DORIAN).HalfStep(I) = _
            RomHalfStepsDorian(I)
    Next I
    For I = TONIC To LEADINGTONE
        gScale(PHRYGIAN).RNumName(I) = RomNamesPhrygian(I)
        gScale(PHRYGIAN).Qual(I) = RomQualPhrygian(I)
        gScale(PHRYGIAN).HalfStep(I) = _
            RomHalfStepsPhrygian(I)
    Next I
    For I = TONIC To LEADINGTONE
        gScale(LYDIAN).RNumName(I) = RomNamesLydian(I)
        gScale(LYDIAN).Qual(I) = RomQualLydian(I)
        gScale(LYDIAN).HalfStep(I) = _
            RomHalfStepsLydian(I)
    Next I
    For I = TONIC To LEADINGTONE
        gScale(MIXOLYDIAN).RNumName(I) = RomNamesMixolydian(I)
        gScale(MIXOLYDIAN).Qual(I) = RomQualMixolydian(I)
        gScale(MIXOLYDIAN).HalfStep(I) = _
            RomHalfStepsMixolydian(I)
    Next I
    gbScalesSetup = True
End Sub

Private Sub InitKey()
    ' (called by sub StoreNotes)
    ' called by sub InitChordsForKey
    ' called by sub DisplaySong
    
    ' calls sub DisplayText

    ' Globals used: gKeyMode, gbSharpKey, gKeyName
    ' Globals set: gKeySig() (used in DisplaySong)

    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim MajKeySharp As Variant
    Dim MajKeyFlat As Variant
    Dim MinKeySharp As Variant
    Dim MinKeyFlat As Variant
    Dim KeySigSharp As Variant
    Dim KeySigFlat As Variant
    Dim E$
    Dim ThisSong As Integer
    
    '************************************
    '************************************

Main:
    GoSub Init1
    GoSub Init2
    GoSub MainLine
    Exit Sub
        
    '************************************
    '************************************

' Depending on the caller, sub InitKey
' must use either gSongCurr or ThisSong.
Init1:
    If gSongCurr > 0 Then ' 15aug08
        ThisSong = gSongCurr
        I = I
    Else
        ThisSong = gSongLast
        I = I
    End If
    Return
    
    '************************************
Init2:
    txtPrint.Text = ""
    ' Major sharp key names
    MajKeySharp = Array("C", "G", "D", _
                    "A", "E", "B", "F#", "C#")
    ' Minor sharp key names
    MinKeySharp = Array("A", "E", "B", _
                    "F#", "C#", "G#", "D#", "A#")
    ' Sharps in the major or minor key signature
    KeySigSharp = Array("", "F#", "C#", _
                    "G#", "D#", "A#", "E#", "B#")

    ' Major flat key names
    MajKeyFlat = Array("C", "F", "Bb", _
                    "Eb", "Ab", "Db", "Gb", "Cb")
        ' Minor flat key names
    MinKeyFlat = Array("A", "D", "G", _
                    "C", "F", "Bb", "Eb", "Ab")
    ' Flats in the major key signature
    KeySigFlat = Array("", "Bb", "Eb", _
                    "Ab", "Db", "Gb", "Cb", "Fb")
    Return
    
    '************************************

MainLine:
    ' gbSharpKey is set in Sub InitHalfStepNames
    If gKeyMode = MAJOR Then
        If gbSharpKey Then
            GoSub MajSharp
        Else
            GoSub MajFlat
        End If
    Else ' assuming Minor (or mediaval modes)
        If gbSharpKey Then
            GoSub MinSharp
        Else
            GoSub MinFlat
        End If
    End If
    GoSub CopyKeySig
    Return
    
    '************************************

MajSharp:
    For I = 0 To 7
        If gKeyName = MajKeySharp(I) Then
            K = I
            GoSub CopySharpSig
            Return
        End If
    Next I
    GoTo Err1
    Return
    
    '************************************

MajFlat:
    For I = 0 To 7
        If gKeyName = MajKeyFlat(I) Then
            K = I
            GoSub CopyFlatSig
            Return
        End If
    Next I
    GoTo Err1
    Return
    
    '************************************

MinSharp:
    For I = 0 To 7
        If gKeyName = MinKeySharp(I) Then
            K = I
            GoSub CopySharpSig
            Return
        End If
    Next I
    GoTo Err1
    Return
    
    '************************************

MinFlat:
    For I = 0 To 7
        If gKeyName = MinKeyFlat(I) Then
            K = I
            GoSub CopyFlatSig
            Return
        End If
    Next I
    GoTo Err1
    Return
    
    '************************************

' Save the letter and accidental in gKeySig.
' Save just the letter in gKeySigName to compare
' against input note names.
CopySharpSig:
    For J = 1 To 7
        gKeySig(J) = "" ' init
        gKeySigName(J) = "" ' init
        gSong(ThisSong).KeySig(J) = ""
    Next J
    If K > 0 Then
        For J = 1 To K
            gKeySig(J) = KeySigSharp(J) ' eg F#,C#
            gSong(ThisSong).KeySig(J) = KeySigSharp(J)
            gKeySigName(J) = _
                Left$(KeySigSharp(J), 1) ' eg F,C
        Next J
    End If
    gKeySigLast = K
    gSong(ThisSong).KeySigUsed = K ' =0 if K=0
    Return
    
    '************************************

CopyFlatSig:
    For J = 1 To 7
        gKeySig(J) = "" ' init
        gKeySigName(J) = "" ' init
        gSong(ThisSong).KeySig(J) = ""
    Next J
    If K > 0 Then
        For J = 1 To K
            gKeySig(J) = KeySigFlat(J)
            gSong(ThisSong).KeySig(J) = KeySigFlat(J)
            gKeySigName(J) = _
                Left$(KeySigFlat(J), 1)
        Next J
    End If
    gKeySigLast = K
    gSong(ThisSong).KeySigUsed = K ' =0 if K=0
    Return
    
    '************************************

' If this is the first key used in this song,
' that is, the key signature key, then save
' its sharps or flats in .KeySigIst(J).
' This will be used later to make sure that
' the naturals to reverse the sharps or flats
' are stored in gNoteName.
' e.g. Key Sig of F Minor has 4 flats:
' Bb, Eb, Ab, Db.  So gNoteName must contain
' the naturals =B, =E, =A, =D in order to
' display any naturals that were input to:
' (1) show the notes used to modulate to a new key;
' (2) show the notes used in a minor key to
' raise the #^4, #^6, or #^7.
CopyKeySig:
    If gSong(ThisSong).NumKey = 1 Then
        For J = 1 To 7
            gSong(ThisSong).KeySigIst(J) = _
                gSong(ThisSong).KeySig(J)
        Next J
        gSong(ThisSong).KeySigUsedIst = _
            gSong(ThisSong).KeySigUsed
    End If
    Return
    
    '************************************

Err1:
    gInputErrs = gInputErrs + 1
    E$ = "InitKey:E1 invalid gKeyname=" & _
        gKeyName & _
        " for gKeyMode=" & gKeyMode & _
        " and gSharpKey=" & gbSharpKey & _
        " in song no=" & ThisSong
    Call DisplayText(E$)
    Debug.Print E$
    I = I
    Stop
    'Return
    
End Sub

Private Function fcnAddAccidentalToInput( _
            pNoteName As String) _
            As String
    ' called by sub StoreVoiceVn
    ' calls
    ' Globals used: gKeySigName(I), gKeySig(I)
    ' Globals set:

    Dim I As Integer
    Dim Letter1$
    Dim Digit1$
    Dim KeySigLetter$

    ' If the input note name letter, e.g. F, is found
    ' in the key signature names, then return
    ' the note name letter plus accidental.
    ' The input note has the form:
    '   note name
    '   optional blank or accidental: " ", b, # (Not =)
    '   digit
    ' e.g. F#3, Bb5, C5, C 5
    
    ' The input note can have a leading accidental
    ' to indicate a note not in the key signature,
    ' e.g. #C4, bB3, bbB3, =A2, xF4
    ' or after use of a natural sign, to indicate
    ' the return to a key signature note, e.g.
    '   KY: D Major, C4, =C4, #C4
   
   
   'If pNoteName = "Bb3" Then
   '    If gSongLast = 29 Then
   '        Debug.Print gSongLast
   '    End If
   'End If
    If pNoteName = "F4" Then
        I = I
    End If
    

    fcnAddAccidentalToInput = pNoteName
    If Len(pNoteName) = 2 Then
        Letter1$ = Left$(pNoteName, 1)
        Digit1$ = Right$(pNoteName, 1)
        GoSub Check
    End If
    If fcnAddAccidentalToInput = "" Then
        I = I
    End If
    Exit Function

    ' Sub InitKey saves the letter and accidental
    ' in gKeySig(), and saves just the letter in
    ' gKeySigName() to compare against input note names.
Check:
    With gSong(gSongLast)
    For I = 1 To .KeySigUsedIst
        KeySigLetter$ = Left$(.KeySigIst(I), 1)
        If Letter1$ = KeySigLetter$ Then
            fcnAddAccidentalToInput = _
                .KeySigIst(I) & Digit1$
            Return
        End If
    Next I
    End With
    Return

' NOTUSED:
Check9:
    For I = 1 To gKeySigLast
        If Letter1$ = gKeySigName(I) Then
            fcnAddAccidentalToInput = gKeySig(I) & Digit1$
            Return
        End If
    Next I
    Return

    End Function

Private Sub InitDoubleNames()
    ' called by Sub Init4Part()
    ' (Called by sub InitChordsForKey)
    ' Only need to call this once.

    ' Globals used:
    ' Globals set: gDblNames(), gDblDescs()

    Dim Names1 As Variant
    Dim Names2 As Variant
    Dim Desc1 As Variant
    Dim Desc2 As Variant
    Dim Desc3 As Variant
    Dim I As Integer
    Dim J As Integer
    Names1 = Array("", "DBL ROOT", _
                "DBL THIRD PRIM RP", "DBL THIRD RP", _
                "FINAL TRIPLE", "DBL PRIM SOP", _
                "DBL SOPRANO", "DBL PRIMARY", _
                "DBL PRIM THIRD MINOR", _
                "DBL ROOT MINOR", "DBL THIRD MINOR", _
                "DBL FIFTH MINOR", _
                "DBL ROOT MAJOR", "DBL THIRD MAJOR", _
                "DBL FIFTH MAJOR", _
                "DBL FIFTH", "DBL DIMINISH")
    Names2 = Array("MAJOR 7TH", _
                    "DOMINANT 7TH", _
                    "HALFDIM 7TH", _
                    "DIMDIM 7TH", "TRIPLEROOT")
    J = 0
    For I = 0 To 16
        gDblNames(J) = Names1(I)
        J = J + 1
    Next I
    For I = 0 To 4
        gDblNames(J) = Names2(I)
        J = J + 1
    Next I

    Desc1 = Array("", "dbl root in bass", _
        "dbl third AND primary tone, minor triad, root pos", _
        "dbl third, minor triad, root pos", _
        "final triple in tonic triad", _
        "dbl soprano AND primary tone, 1st inv", _
        "dbl soprano, 1st inv", _
        "dbl primary tone 1^ 4^ 5^, 1st inv", _
        "dbl third AND primary tone, minor triad, 1st inv")
    ' apparently only 9 continuations allowed
    Desc2 = Array("dbl root, minor triad, 1st inv", _
        "dbl third, minor triad, 1st inv", _
        "dbl fifth, minor triad, 1st inv", _
        "dbl root, major triad, 1st inv", _
        "dbl third, major triad, 1st inv", _
        "dbl fifth, major triad, 1st inv", _
        "dbl fifth, 2nd inv", _
        "diminished triad: soprano 5th, else bass 3rd", _
        "maj-maj7 7th chord: I7,IV7")
    Desc3 = Array("dominant 7th chord: ii7, V7", _
        "dim-min7 halfdim 7th chord: vii/o7", _
        "dim-dim7 dimdim 7th chord: viio7", _
        "triple root in any inversion")

    J = 0
    For I = 0 To 8
        gDblDescs(J) = Desc1(I)
        J = J + 1
    Next I
    For I = 0 To 8
        gDblDescs(J) = Desc2(I)
        J = J + 1
    Next I
    For I = 0 To 3
        gDblDescs(J) = Desc3(I)
        J = J + 1
    Next I
End Sub



Private Sub InitNoteNames()
    
    ' called by sub InitChordsForKey

    ' Initialize gNoteName array with string names
    ' of the notes from C0,C#0,D0,...,C8
    ' where C4 is middle C.  C0 is in gNoteName(0)
    ' and C8 is in gNoteName(NOTESMAX=96).
    ' Dependent on gKeyName.
    ' NOTICE - In order to allow for A0 as the lowest
    ' note of the piano's 88 keys, I start the table
    ' with C0 to B0, followed by C1 to B1, where C1 is
    ' the lowest C on the piano.
    Dim I As Integer
    Dim K As Integer
    Dim N As Integer
    Dim SavegbDebugPrint As Boolean
    
'*******************************************
'*******************************************
    
Main:
    GoSub Init
    GoSub MainLine
    Exit Sub
    
'*******************************************
'*******************************************

Init:
    SavegbDebugPrint = gbDebugPrint
    gbDebugPrint = False
    
    For I = NOTESMIN To NOTESMAX ' Clear the array
        gNoteName(I) = ""
    Next I
    If gHalfStepName(0) = "B#" Then
        I = I
    End If
    Return
    
'*******************************************
    
MainLine:
    GoSub Octaves
    GoSub AdjustBC
    GoSub AdjustCb
    gbDebugPrint = SavegbDebugPrint
    Return
    
'*******************************************
    
Octaves:
    ' Array gHalfStepName was filled in sub
    ' InitHalfStepNames using gKeyName.
    N = 0 ' 0 to 95 = (1+NUMOCTAVES) * 12 - 1 = 95
    For K = 0 To NUMOCTAVES ' 0 to 7
        For I = 0 To 11 ' 12 half-steps per octave
            gNoteName(N) = gHalfStepName(I) & K
            N = N + 1
        Next I
    Next K
    ' N = (1+NUMOCTAVES) * 12 + 1 = 96 for "C8"
    ' K = NUMOCTAVES + 1 = 8
    gNoteName(N) = gHalfStepName(0) & K ' "C8" at 96
    If gbDebugPrint Then
        GoSub Debug1
    End If
    Return
    
    
'*******************************************
    
AdjustBC:
    ' In the keys of C# major (7 sharps) or
    ' C# minor (4 sharps with raised A# and B#),
    ' B#1 is the enharmonic of C1 (same pitch),
    ' but I choose the convention of making
    ' B#0=12 one step above B0=11 and one step
    ' below C#1=13.
    ' Otherwise, it would be confusing to code
    ' B#5=60 next to C#5=61.
    If gHalfStepName(0) = "B#" Then
        K = 0
        For I = 12 To 96 Step 12
            gNoteName(I) = "B#" & K
            K = K + 1
        Next I
    End If
    Return
    
'*******************************************
        
AdjustCb:
    ' In the keys of Cb major (7 flats) or
    ' Cb minor (4 flats with raised G# and A#),
    ' Cb1 is the enharmonic of B1 (same pitch),
    ' but I choose the convention of making
    ' Cb1=11 one step above Bb0=10 and two steps
    ' below Db1=13.
    If gHalfStepName(11) = "Cb" Then
        K = 1
        For I = 11 To 95 Step 12
            gNoteName(I) = "Cb" & K
            K = K + 1
        Next I
    End If
    Return
    
'*******************************************

Debug1:
    If gbDebugPrint Then
        Debug.Print "InitNoteNames:D1 "; _
            "gKeyName gKeyMode="; gKeyName; gKeyMode;
        N = 0 ' 0 to 95 = (1+NUMOCTAVES) * 12 - 1 = 95
        For K = 0 To NUMOCTAVES ' 0 to 7
            For I = 0 To 11 ' 12 half-steps per octave
                Debug.Print gNoteName(N); " ";
                N = N + 1
            Next I
        Next K
        ' N = (1+NUMOCTAVES) * 12 + 1 = 96 for "C8"
        Debug.Print gNoteName(N)
    End If
    Return
    
End Sub

Private Sub InitNoteNamesCMajor()
    ' called by sub Main4Part
    
    ' calls sub InitHalfStepNames
    
    ' This sub InitNoteNamesCMajor is like
    ' sub InitNoteNames, but it is meant to be
    ' used for the notes listed in
    '   OP: MISC RANGE F2 G3
    ' which is specified in C Major, no matter
    ' what the key of the current song is.

    ' Initialize gNoteName array with string names
    ' of the notes from C0,C#0,D0,...,C8
    ' where C4 is middle C.  C0 is in gNoteName(0)
    ' and C8 is in gNoteName(NOTESMAX=96).
    ' Dependent on gKeyName.
    ' NOTICE - In order to allow for A0 as the lowest
    ' note of the piano's 88 keys, I start the table
    ' with C0 to B0, followed by C1 to B1, where C1 is
    ' the lowest C on the piano.
    Dim I As Integer
    Dim K As Integer
    Dim N As Integer
    
 '*********************************************
'*********************************************
Main:
    GoSub Init
    GoSub MainLine
        'call InitHalfStepNames
        'fcnToIxCMajor(gKeyNote)
    Exit Sub

'*********************************************
'*********************************************

Init:
    For I = NOTESMIN To NOTESMAX ' Clear
        gNoteNameCMajor(I) = ""
    Next I
    'gbDebugPrint = True
    Return
    
'*********************************************

MainLine:
    gKeyName = "C"
    gKeyMode = MAJOR ' 1=Major,...
    ' gKeyName is set as in sub cmdChords_Click.
    Call InitHalfStepNames ' [gKeyName] before fcnToIxCMajor
    GoSub Octaves
    GoSub GetKeyNote
    Return
    
'*********************************************

Octaves:
    ' Array gHalfStepName was filled in sub
    ' InitHalfStepNames using gKeyName.
    N = 0 ' 0 to 95 = (1+NUMOCTAVES) * 12 - 1 = 95
    For K = 0 To NUMOCTAVES ' 0 to 7
        For I = 0 To 11 ' 12 half-steps per octave
            gNoteNameCMajor(N) = gHalfStepName(I) & K
            N = N + 1
        Next I
    Next K
    ' N = (1+NUMOCTAVES) * 12 + 1 = 96 for "C8"
    ' K = NUMOCTAVES + 1 = 8
    gNoteNameCMajor(N) = gHalfStepName(0) & K ' "C8" at 96
    If gbDebugPrint Then
        GoSub Debug1
    End If
    Return
    
'*********************************************

GetKeyNote:
    ' For insurance:
    gKeyNote = gKeyName & "0"
    ' Use function fcnToIxCMajor to return the index of
    ' the note, e.g. C0, in gNoteName.
    gKeyIndex = fcnToIxCMajor(gKeyNote) ' call
    If gKeyIndex = 99 Then
        GoTo Err1
    End If
    Return
    
'*********************************************

Debug1:
    If gbDebugPrint Then
        Debug.Print "InitNoteNamesCMajor:D1 "; _
            "gKeyName gKeyMode="; gKeyName; gKeyMode;
        N = 0 ' 0 to 95 = (1+NUMOCTAVES) * 12 - 1 = 95
        For K = 0 To NUMOCTAVES ' 0 to 7
            For I = 0 To 11 ' 12 half-steps per octave
                Debug.Print gNoteNameCMajor(N); " ";
                N = N + 1
            Next I
        Next K
        ' N = (1+NUMOCTAVES) * 12 + 1 = 96 for "C8"
        Debug.Print gNoteNameCMajor(N)
    End If
    Return
    
'*********************************************

Err1:
    Debug.Print "InitNoteNamesCMajor:E1 invalid gKeynote="; _
        gKeyNote; "for C Major"
    I = I
    Stop

End Sub

Private Sub InitPrimaryTones()
    ' called by sub InitChordsForKey
    ' Globals set: gPrimaryTones()
    ' Globals used: gKeyIndex

    ' Enter with gKeyIndex, the half-step index
    ' to the lowest note of the Key, e.g. "G#0",
    ' in gNoteName, and then flag the slots
    ' in gPrimaryTones of each scale degree
    ' 1^ with 1, 4^ with 4, 5^ with 5.

    ' C C# D D# E F F# G G# A A# B   CHROMATICS
    ' 0 1  2 3  4 5 6  7 8  9 10 11  HALF STEPS
    ' 1    2    3 4    5    6    7   CHORD STEPS
    ' 1^   2^   3^4^   5^   6^   7^  SCALE DEGREES
                            ' in the key of C Major.

    ' NOTE that there is no need to modify this
    ' for Major or the various Minor scales.
    ' See Rom Num values in proc Init.
    ' The Primary Tones are the same in every
    ' scale: major, minor, dorian, phrygian,
    ' lydian, mixolydian.
    ' The half steps are: 0 x x 5 7.

    Dim I As Integer
    ' NOTESMIN -1, the index of Cb0=B(-1).
    For I = NOTESMIN To NOTESMAX ' Clear the array
        gPrimaryTones(I) = 0
    Next I
    ' 1,4,5 are the scale degrees 1^,4^,5^
    For I = gKeyIndex To NOTESMAX Step 12
        gPrimaryTones(I) = 1
    Next I
    For I = gKeyIndex + 5 To NOTESMAX Step 12
        gPrimaryTones(I) = 4
    Next I
    For I = gKeyIndex + 7 To NOTESMAX Step 12
        gPrimaryTones(I) = 5
    Next I
End Sub

Private Sub InitMinorAccidentals(pIdx As Integer)
    ' called by Sub InitNeverDouble
    
    ' Globals used: gKeyIndex, gKeyMode
    ' Globals set: gNeverDouble()

    ' Enter with pIdx, the index of a 7th, 6th,
    ' or 3rd of a melodic minor scale in gNoteName(pIdx).
    ' Raise it a half-step depending on the preceding
    ' note.
    
    ' If gKeyMode is Major, flag the leading tone
    ' scale degree 7^ with 7.
    ' If gKeyMode is MINORMELUP or _
    ' ascending melodic minor, also flag
    ' scale degree 6^ with 6, indicating a raised
    ' scales step #6^ and #7^.

    ' A A# B C  C# D D# E F  F# G  G# A   CHROMATICS
    ' 0 1  2 3  4  5 6  7 8  9  10 11 12  HALF STEPS
    ' 1    2 3     4    5 6     7     8/1 CHORD STEPS
    ' 1   ^2^3    ^4   ^5^6    ^7    ^8   SCALE DEGREES
                            ' in the key of A Minor.
    '         #^3          #^6   #^7      Altered
    ' Note #^3 = C# = half step 4  in A minor (Picardy 3)
    ' Note #^6 = F# = half step 9  in A minor,
    ' Note #^7 = G# = half step 11 in A minor.

    ' NOTE that there is no need to modify this
    ' for Major or the various Minor scales.
    ' See Rom Num values in proc InitTriads.

    ' TODO check this for the other modes:

    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim Prev$
    Dim Letter$
    Dim Digit$
    Dim E$
    Dim Song As Integer
    
'*********************************************
'*********************************************
Main:
    GoSub MainLine
    'GoSub Debug2
    'GoSub Debug1
    Exit Sub

'*********************************************

' gNoteName format: C4 or C#4 or Cb4.
' Get the note (letter and digit) that is one half step below
' the altered note
'    #^4, #^6, #^7
'  and store it in Prev$.
' If that accidental is "b", precede the altered note
' with the natural sign "=".
' If that accidental is "#", precede the altered note
' with the double sharp sign "x".
' If there is no accidental, precede the altered note
' with the sharp sign "#".

MainLine:
    Prev$ = gNoteName(pIdx - 1)
    Letter$ = Left$(Prev$, 1)
    Digit$ = Right$(Prev$, 1)
    'If Left$(Prev$, 2) = "Bb" Then
    '    I = I
    'End If
    If InStr(Prev$, "#") Then
        gNoteName(pIdx) = "x" & Letter$ & Digit$
    ElseIf InStr(Prev$, "b") Then
        gNoteName(pIdx) = "=" & Letter$ & Digit$
    Else
        gNoteName(pIdx) = "#" & Letter$ & Digit$
    End If
    Return

'*********************************************

Debug1:
    Debug.Print "InitMinorAccidentals: "
    K = gKeyIndex
    For J = 0 To 7
        For I = 0 To 11
            If K < 96 Then
                Debug.Print K; gNoteName(K);
                K = K + 1
            End If
        Next I
        Debug.Print
    Next J
    Debug.Print K; gNoteName(K)
    Return

'**********************************************
    
Debug2:
    If gSongCurr > 0 Then
        Song = gSongCurr
    Else
        Song = gSongLast
    End If
    E$ = "InitMinorAccidentals: " _
        & "accidentals for key=" _
        & gKeyName & " for mode=" & gKeyMode _
        & " in song=" & Song _
        & " gNoteX=" & gNoteX
    Call DisplayText(E$)
    Debug.Print E$
    Debug.Print "InitMinorAccidentals: gTextLine="; _
        gTextLine
    'GoSub Debug1
    Return

End Sub

Private Sub InitNeverDouble()
    ' called by sub InitChordsForKey
    
    ' Globals used: gKeyIndex, gKeyMode
    ' Globals set: gNeverDouble()

    ' Enter with gKeyIndex, the half-step index
    ' to the lowest note of the Key, e.g. "G#0",
    ' in gNoteName, and then flag the slots
    ' in gNeverDouble of note not to be doubled.

    ' If gKeyMode is Major, flag the leading tone
    ' scale degree 7^ with 7.
    ' If gKeyMode is MINORMELUP or _
    ' ascending melodic minor, also flag
    ' scale degree 6^ with 6, indicating a raised
    ' scales step #6^ and #7^.

    ' C C# D D# E F F# G G# A A# B  C     CHROMATICS
    ' 0 1  2 3  4 5 6  7 8  9 10 11 12/0  HALF STEPS
    ' 1    2    3 4    5    6    7  8/1   CHORD STEPS
    ' 1^   2^   3^4^   5^   6^   7^ 8^/1^ SCALE DEGREES
                        '  in the key of C Major.

    ' A A# B C  C# D D# E F  F# G  G# A   CHROMATICS
    ' 0 1  2 3  4  5 6  7 8  9  10 11 12  HALF STEPS
    ' 1    2 3     4    5 6     7     8/1 CHORD STEPS
    ' 1   ^2^3    ^4   ^5^6    ^7    ^8   SCALE DEGREES
                            ' in the key of A Minor.
    '         #^3          #^6   #^7      Altered
    ' Note #^3 = C# = half step 4  in A minor (Picardy 3)
    ' Note #^6 = F# = half step 9  in A minor,
    ' Note #^7 = G# = half step 11 in A minor.


    ' NOTE that there is no need to modify this
    ' for Major or the various Minor scales.
    ' See Rom Num values in proc InitTriads.

    ' TODO check this for the other modes:

    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim Prev$
    Dim Letter$
    Dim Digit$
    Dim E$
    Dim Idx As Integer
    Dim Song As Integer
    
'*********************************************
'*********************************************
Main:
    GoSub Init
    GoSub MainLine
        'GoSub AdjustAccidental
    'GoSub Debug2
    Exit Sub

'*********************************************
'*********************************************

Init:
    For I = NOTESMIN To NOTESMAX ' Clear the array
        gNeverDouble(I) = 0
    Next I
    Return
    
'*********************************************

MainLine:
    Select Case gKeyMode
    Case MAJOR, LYDIAN
        For Idx = gKeyIndex + 11 To NOTESMAX Step 12
            gNeverDouble(Idx) = 7 ' 7^
        Next Idx
    Case MINORNAT, MINORHARM, MINORMELUP
        'GoSub Debug1
        For Idx = gKeyIndex + 11 To NOTESMAX Step 12
            gNeverDouble(Idx) = 7 ' 7^
            Call InitMinorAccidentals(Idx)
        Next Idx
        For Idx = gKeyIndex + 9 To NOTESMAX Step 12
            gNeverDouble(Idx) = 6 ' 6^
            Call InitMinorAccidentals(Idx)
        Next Idx
        
        ' In Minor, the last chord in the piece
        ' can raise the third of the tonic chord
        ' a half step, changing the chord from
        ' i to I.  This is called the Picardy third.
        ' It is a more satisfying final cadence to
        ' a piece in Minor.
        
        ' TODO 12/15/6
        ' The raised third MAY also be used in some
        ' cases where a non-harmonic tone NHT
        ' Lower Neighbor commonly goes down a
        ' half step, e.g in key of C minor (3 flats)
        ' the pattern: F4, =E4, F4  where =E4 is
        ' used instead of the key signature Eb4.
        ' I may be wrong, because this case may
        ' be normally limited to a #^7 instead of
        ' a #^3.
        For Idx = gKeyIndex + 4 To NOTESMAX Step 12
            gNeverDouble(Idx) = 3 ' Picardy third 3^
            Call InitMinorAccidentals(Idx)
        Next Idx
    End Select
    Return
    
'**********************************************
    
Debug2:
    If gSongCurr > 0 Then
        Song = gSongCurr
    Else
        Song = gSongLast
    End If
    E$ = "InitNeverDouble: " _
        & "accidentals for key=" _
        & gKeyName & " for mode=" & gKeyMode _
        & " in song=" & Song _
        & " gNoteX=" & gNoteX
    Call DisplayText(E$)
    Debug.Print E$
    Debug.Print gTextLine
    
    'GoSub Debug1
    Return

End Sub


Private Sub InitScaleDegree()
    ' called by sub InitChordsForKey
    ' Globals used: gKeyIndex
    ' Globals set: gScaleDegree()

    ' gScaleDegree is used in fcnbAlteredFB.

    ' Enter with gKeyIndex, the half-step index
    ' to the lowest note of the Key, e.g. "G#1",
    ' in gNoteName, and then flag the slots
    ' with the triad scale degrees on Octave0
    ' starting at the G#1 slot.

    ' C C# D D# E F F# G G# A A# B  C     CHROMATICS
    ' 0 1  2 3  4 5 6  7 8  9 10 11 12/0  HALF STEPS
    ' 1    2    3 4    5    6    7  8/1   CHORD STEPS
    ' 1   ^2   ^3^4TT ^5   ^6   ^7 ^8   SCALE DEGREES
                        '  in the key of C Major.
    ' 1 ^2^2 ^3^3^4^TT^5 ^6^6 ^7^7 ^8/1 SCALE DEGREES
    ' in the key of C Major including
    ' major and minor ^2, ^3, ^6, ^7's,
    ' and perfect ^4's, ^5's, ^8's.

    ' I arbitrarily indicate the tritone (TT) interval
    ' with a -5 scale degree.  It may be useful.

    ' NOTE that there is no need to modify this
    ' for Major or the various Minor scales.

    Dim I As Integer
    Dim J As Integer
    Dim Octave0 As Variant
    Octave0 = Array(1, 2, 2, 3, 3, 4, -5, 5, 6, 6, 7, 7)

    For I = NOTESMIN To NOTESMAX ' =96. Clear the array
        gScaleDegree(I) = 0
    Next I
    For I = 0 To 11
        For J = gKeyIndex + I To NOTESMAX Step 12
            gScaleDegree(J) = Octave0(I)
        Next J
    Next I
End Sub

Private Sub MainChordsForKey()
    
    ' called by sub cmdChords_Click
    ' called by sub WriteAllChords
    ' calls sub InitTriads
    ' calls sub ScanTriads
    ' calls sub PrintDblTypes
    ' calls sub DisplayText

    ' Globals used: gKeyMode, gChordLast
    ' Globals set:
    '   gKeyChords(gsongkey).FirstChord(gScaleDeg)
    '   gKeyChords(gsongkey).FinalChord (gScaleDeg)

' See MainChordsForKey and fcnbCandChord
'Private Type KeyChordsRec
'    .FirstChord(1 To 7) As Integer ' index of the
'    ' first chord in gChord of this scale degree.
'    .FinalChord(1 To 7) As Integer ' index of  the
'    ' last chord in gChord of this scale degree.
'End Type
'Dim gKeyChords(1 To NUMKEYMAX) As KeyChordsRec

    Dim E$
    Dim I As Integer

    
'*********************************************
'*********************************************

Main:
    GoSub MainLine
    GoSub Debug1
    GoSub Debug2
    Exit Sub
    
'*********************************************
'*********************************************
    
MainLine:
    ' gSongKey is set in sub cmdChords_Click.
    For gScaleDeg = TONIC To LEADINGTONE
        gKeyChords(gSongKey).FirstChord(gScaleDeg) = _
            gChordLast ' used in sub fcnbCandChord
        
        Call InitTriads ' Only once: gScaleDeg
        '   [gKeyIndex, gKeyMode, gScaleDeg]
        If gbDebugPrint Then
            Debug.Print
        End If
        
        Call ScanTriads ' gScaleDeg
            'If fcnbValidChord(B, T, A, S) Then
                'call EditChord(B, T, A, S)
        
        Call PrintDblTypes ' [gScaleDeg]
        If gChordLast > _
        gKeyChords(gSongKey).FirstChord(gScaleDeg) _
        Then
            gKeyChords(gSongKey).FirstChord(gScaleDeg) = _
                gKeyChords(gSongKey).FirstChord(gScaleDeg) _
                + 1
            gKeyChords(gSongKey).FinalChord(gScaleDeg) = _
                gChordLast ' used in sub fcnbCandChord
            ' TODO CLEANUP
            'If gScaleDeg = FIFTH Then
            '    Debug.Print gKeyMode; gScaleDeg; _
            '    gKeyChords(gsongkey).FirstChord(gScaleDeg); _
            '    gKeyChords(gsongkey).FinalChord(gScaleDeg)
            '    I = I
            'End If
        Else
            GoSub Err1
            I = I
            'Stop
        End If
    Next gScaleDeg
    Return

'*********************************************

Debug1:
    gGrandTotChordsValid = gGrandTotChordsValid + _
        gTotChordsValid
    E$ = "MainChordsForKey:D1 Key=" & _
        gKeyName & _
        " gKeyMode=" & gKeyMode & _
        " Valid=" & _
        gTotChordsValid & ", " & _
        gTotChordsChecked & _
        "=Grand Total chords checked"
    Call DisplayText(E$)
    Debug.Print E$
    Return

'*********************************************

Debug2:
    gbDebugPrintSave = gbDebugPrint
    'gbDebugPrint = True
    If gbDebugPrint Then ' 15jul08
        Debug.Print "MainChordsForKey:D2 Key="; _
            gKeyName; _
            " gKeyMode="; gKeyMode; _
            " Valid="; _
            gTotChordsValid; _
            gTotChordsChecked; _
            "=grand Total chords checked"
        For I = 1 To DBLTYPEMAX
            Debug.Print I; gDblType(I).TotVal; _
                gDblDescs(I)
        Next I
    End If
    gbDebugPrint = gbDebugPrintSave
    Return
    
'*********************************************

Err1:
    Debug.Print "MainChordsForKey:E1 invalid "; _
        "gChordLast="; gChordLast; _
        "for gScaleDeg="; gScaleDeg

    Debug.Print "MainChordsForKey:E1 gScaleDeg ptrs=";
    For gScaleDeg = TONIC To LEADINGTONE
        Debug.Print "E1: gScaleDeg="; gScaleDeg; _
            gKeyChords(gSongKey).FirstChord(gScaleDeg); _
            gKeyChords(gSongKey).FinalChord(gScaleDeg); _
            "  ";
    Next gScaleDeg
    Debug.Print
    I = I
    Return
    
End Sub

Private Sub InitTriads()
    ' called by sub MainChordsForKey
    ' called by sub MakeChordFromInputNotes
    
    ' Globals used: gKeyIndex [index(G0)],
    '       gKeyMode [1=MAJOR,2=MINOR,...],
    '       gScaleDeg [1-7]
    ' Globals set: gRomNumName =, gQuality =,
    '       gTriads(I)

    ' TODO 6aug08 add support for the medieval modes
    ' in Sub InitTriads using variants like
    '   Incr7InMajor = Array(0,...
    
    ' This sub is called for each of the 7
    ' scale degrees from tonic to leadingtone.
    ' For each scale degree,
    ' insert chord position numbers 1,3,5,7 at the
    ' proper half-step number 0-11, one per
    ' octave for eight octaves (C0),A0,B0,C1 to C8,
    ' from KeyIndexA0 to NOTESMAX.
    ' Fill C0-B0 in the first octave so that
    ' the starting point, e.g.
    ' Index1 = fcnToIx("G0")+{0,2,4,5,7,9,11}
    ' falls before octaves C1-C8.

    ' Enter with gKeyIndex, the half-step index
    ' to the lowest note of the Key, e.g. "G#0",
    ' in gNoteName, and then flag the proper
    ' slots in gTriads with chord numbers 1,3,5,7.
    ' E.g. flag the slots in gTriads of all the
    ' G's with 1, B's with 3, D's with 5, F's with
    ' 7 in the key of G.

    ' In a minor key, after the triads are flagged,
    ' insert triad numbers for the #^6 and #^7
    ' scale degrees.  E.g. if the #^6 occurs a half
    ' step above the 3rd of D-F-A, the iv triad of
    ' the key of A minor, then make the #^6 equal
    ' to a THIRD = 3, to indicate IV D-F#-A.

    ' Per Ottman, p 283, in minor, e.g. A minor,
    ' don't raise the 5th (#^6) of iio BDF to
    ' ii BDF#, because #^6 = F# resolves upward to
    ' #^7 = G#. The problem is that the seventh of
    ' the chord BDFA resolves downward to G# = #^7,
    ' resulting in a doubled leading tone G#.

    ' Per Ottman, p 208, in minor, e.g. A minor,
    ' the triad built on #^6 = F#AC vio is seldom
    ' seen.
    
    ' C C# D D# E F F# G G# A A# B  C  CHROMATICS
    ' 0 1  2 3  4 5 6  7 8  9 10 11 12 HALF STEPS
    ' 1    2    3 4    5    6    7  8/1 CHORD STEPS
    ' 1         3      5         7  8/1 CHORD TRIADS
    '^1   ^2   ^3^4   ^5   ^6   ^7 ^8 SCALE DEGREES
                            ' in the key of C Major.

    ' A A# B C  C# D D# E F  F# G  G# A   CHROMATICS
    ' 0 1  2 3  4  5 6  7 8  9  10 11 12  HALF STEPS
    ' 1    2 3     4    5 6     7     8/1 CHORD STEPS
    ' 1      3          5       7     8/1 CHORD TRIADS
    ' 1   ^2^3    ^4   ^5^6    ^7    ^8   SCALE DEGREES
                            ' in the key of A Minor.
    '         #^3          #^6   #^7      Altered
    ' 1      3  3       5 6  6  7  7      Altered Triads
    ' Note #^3 = C# = half step 4  in A minor (Picardy 3)
    ' Note #^6 = F# = half step 9  in A minor,
    ' Note #^7 = G# = half step 11 in A minor.

    Const INCRMIN3RD As Integer = 3
    Const INCRMAJ3RD As Integer = 4
    Const INCRDIM5TH As Integer = 6
    Const INCRMAJ5TH As Integer = 7
    Const INCRAUG5TH As Integer = 8
    Const INCRDIM7TH As Integer = 9
    Const INCRDOM7TH As Integer = 10
    Const INCRMAJ7TH As Integer = 11
    Dim I As Integer
    Dim K As Integer
    Dim Index1 As Integer
    Dim Incr As Integer
    Dim Incr3rd As Integer
    Dim Incr5th As Integer
    Dim Incr7th As Integer
    Dim KeyIndexA0 As Integer

    Const RAISED3RD As Integer = 4
    Const RAISED6TH As Integer = 9
    Const RAISED7TH As Integer = 11
'   Not needed?
    Dim bRaisedThird As Boolean
    Dim bRaisedSixth As Boolean
    Dim bRaisedSeventh As Boolean
    
    Dim Incr7InMajor As Variant
    Dim Incr7InMinor As Variant
    Dim Incr7InMajD7 As Variant
    Dim Incr7InMinD7 As Variant
    
'*********************************************
'*********************************************
Main:
    GoSub Init
    GoSub GetQuality
    GoSub MainLine
    If gKeyMode = MINORNAT Then
        If gChordLast = 1818 Then
            GoSub Debug1
            GoSub RaisedStep
            GoSub Debug1
        Else
            GoSub RaisedStep
        End If
    End If
    Exit Sub

'*********************************************
'*********************************************

Init:
    For I = NOTESMIN To NOTESMAX ' Clear the array
        gTriads(I) = 0
    Next I
    gRomNumName = _
        gScale(gKeyMode).RNumName(gScaleDeg)
        
    ' Following include Major7ths and Dominant 7ths
    '                       I   ii  iii IV  V   vi  vii
    Incr7InMajor = Array(0, 11, 10, 10, 11, 10, 10, 10)
    Incr7InMinor = Array(0, 10, 10, 11, 10, 10, 11, 10)
    
    ' Following include only dominant sevenths, eg V7
    Incr7InMajD7 = Array(0, 0, 10, 10, 0, 10, 10, 10)
    Incr7InMinD7 = Array(0, 10, 10, 0, 10, 10, 0, 10)
    Return


'*********************************************

GetQuality:
    gQuality = gScale(gKeyMode).Qual(gScaleDeg)
    Select Case gQuality
    Case QUALMAJOR
        Incr3rd = INCRMAJ3RD ' 4
        Incr5th = INCRMAJ5TH ' 7
        Incr7th = INCRDOM7TH ' 10 Dominant 7
        'Incr7th = INCRMAJ7TH ' 11 Tonic, Subdominant
    Case QUALMINOR
        Incr3rd = INCRMIN3RD ' 3
        Incr5th = INCRMAJ5TH ' 7
        Incr7th = INCRDOM7TH ' 10 Supertonic, Mediant, Submediant
    Case QUALDIM
        Incr3rd = INCRMIN3RD ' 3
        Incr5th = INCRDIM5TH ' 6
        ' half-dim 7th: B D F A is dim - min 7th
        Incr7th = INCRDOM7TH ' 10 Leadingtone
    Case QUALAUGM
        Incr3rd = INCRMAJ3RD ' 4
        Incr5th = INCRAUG5TH ' 8
        Incr7th = 0 ' very rare to use the 7th Ott p30
    Case Else
        Debug.Print _
            "InitTriads: invalid gQuality="; _
            gQuality
        Stop
    End Select
    Return
    'Case QUALDIMDIM
    '    Incr3rd = 3
    '    Incr5th = 6
         ' dim 7th chord: B D F Ab is dim - dim 7th
    '    INCRDIM7TH = 9 ' flatted dominant 7th

'*********************************************

MainLine:
    KeyIndexA0 = fcnToIx("A0")
    If KeyIndexA0 = 99 Then
        KeyIndexA0 = fcnToIx("=A0")
        If KeyIndexA0 = 99 Then
            I = I
            Stop
        End If
    End If
    ' The diatonic chord numbers 1,3,5 are
    ' from the chord numbers 1-8 of an octave.
    
    ' Remember:
    '   gKeyName e.g. Cb, C, C#
    '   gKeyNote = gKeyName & "0"
    '   gKeyIndex = fcnToIx(gKeyNote)

    Index1 = gKeyIndex + _
        gScale(gKeyMode).HalfStep(gScaleDeg)
    'While Index1 > KeyIndexA0
    '    Index1 = Index1 - 12
    'Wend
    If Index1 > KeyIndexA0 Then
        Index1 = Index1 - 12
    End If
    If Index1 > KeyIndexA0 Then
        Index1 = Index1 - 12
        ' subtract twice only for "B0"
    End If

    For I = Index1 To NOTESMAX Step 12
        If I >= KeyIndexA0 Then
            gTriads(I) = FIRST ' 1 of chord
        End If
    Next I
    For I = Index1 + Incr3rd To NOTESMAX Step 12
        If I >= KeyIndexA0 Then
            gTriads(I) = THIRD ' 3
        End If
    Next I
    For I = Index1 + Incr5th To NOTESMAX Step 12
        If I >= KeyIndexA0 Then
            gTriads(I) = FIFTH ' 5
        End If
    Next I
    'GoSub OldIncr7
    GoSub NewIncr7
    Return
    
'*********************************************

NewIncr7:
    'If gbMajorSeventhExcluded Then
    '  If gKeyMode = MAJOR Then
    '      Incr = Incr7InMajD7(gScaleDeg)
    '      GoSub Increment7th
    '  ElseIf gKeyMode = MINORNAT Then
    '      Incr = Incr7InMinD7(gScaleDeg)
    '      GoSub Increment7th
    '  End If
    'Else
      If gKeyMode = MAJOR Then
          Incr = Incr7InMajor(gScaleDeg)
          GoSub Increment7th
      ElseIf gKeyMode = MINORNAT Then
          Incr = Incr7InMinor(gScaleDeg)
          GoSub Increment7th
      End If
    'End If
    'GoSub Debug1
    Return

'*********************************************

' NOT USED but apparently good enough. 6aug08
OldIncr7:
    ' Dominant 7th V7:
    If gScaleDeg = DOMINANT _
    And (gQuality = QUALMAJOR _
      Or gQuality = QUALMINOR) Then
        Incr = Incr7th ' INCRDOM7TH = 10
        GoSub Increment7th
        Return
    End If
    
    ' Supertonic 7th ii7:
    If gScaleDeg = SUPERTONIC _
    And gQuality = QUALMINOR Then
        Incr = Incr7th ' INCRDOM7TH = 10
        GoSub Increment7th
        Return
    End If
    
    'If gScaleDeg = 1 Or gScaleDeg = 4 Then
    ' Major 7th= M7 e.g. I7 or IV7
    If gQuality = QUALMAJOR Then
        If gScaleDeg = TONIC _
        Or gScaleDeg = SUBDOMINANT Then
            Incr = INCRMAJ7TH ' =11
            GoSub Increment7th ' 25aug08
            Return
        End If
    End If

    ' Allow any inversion for half-dim 7th, per Ottman
    ' examples in chap 13 p 278 ff.
    ' Diminished iio 7 in Minor or viio 7 in Major.
    If gQuality = QUALDIM Then
        Incr = Incr7th
        GoSub Increment7th
        Return
    End If

    ' This in included for completeness, although
    ' QUALDIMDIM does not come from the gScales.
    ' Diminished 7th = d7 e.g. viio65
    If gQuality = QUALDIMDIM Then
        Incr = INCRDIM7TH ' 9
        GoSub Increment7th
        Return
    End If
    Return

'*********************************************

Increment7th:
    For I = Index1 + Incr _
        To NOTESMAX Step 12
        If I >= KeyIndexA0 Then
            gTriads(I) = SEVENTH ' 7
        End If
    Next I
    Return
    
'*********************************************

RaisedStep:
    ' Picardy 3rd can be used at cadence of Minor piece.
    bRaisedThird = False
    bRaisedSixth = False
    bRaisedSeventh = False
    'If gKeyMode <> MAJOR Then
    'If gKeyMode = MINORMELUP Then
        For I = gKeyIndex + RAISED3RD To NOTESMAX _
        Step 12
            If gTriads(I - 1) >= FIRST _
            And gTriads(I - 1) <= FIFTH Then
                gTriads(I) = gTriads(I - 1)
                bRaisedThird = True
            End If
        Next I
        
        For I = gKeyIndex + RAISED6TH To NOTESMAX _
        Step 12
            If gTriads(I - 1) >= FIRST _
            And gTriads(I - 1) <= FIFTH Then
                gTriads(I) = gTriads(I - 1)
                bRaisedSixth = True
            End If
        Next I
        
        For I = gKeyIndex + RAISED7TH To NOTESMAX _
        Step 12
            If gTriads(I - 1) >= FIRST _
            And gTriads(I - 1) <= FIFTH Then
                gTriads(I) = gTriads(I - 1)
                bRaisedSeventh = True
            End If
        Next I
    'End If
   ' Not time for this yet.  See begin of fcnbValidChord.
   'If bRaisedSixth Then
   '    gRomNumName = _
   '        gScale(MINORMELUP).RNumName(gScaleDeg)
   'End If
   'If bRaisedSeventh Then
   '    gRomNumName = _
   '        gScale(MINORHARM).RNumName(gScaleDeg)
   'End If
   'If gScaleDeg = 2 Then
   '    Stop
   'End If
   Return

'*********************************************

Debug1:
    Debug.Print "InitTriads:D1 "; _
        "gSongCurr gNoteX gKeyName gKeyMode gScaledeg="; _
        gSongCurr; gNoteX; gKeyName; gKeyMode; gScaleDeg
    K = 0
    Debug.Print "gtriads(i)=";
    For I = NOTESMIN To NOTESMAX
        If K < 12 Then
            K = K + 1
        Else
            K = 1
            Debug.Print
            Debug.Print "gtriads(i)=";
        End If
        Debug.Print I; gTriads(I);
    Next I
    Debug.Print
    Return
    
'*********************************************

End Sub

Private Sub InitTriads8()
    ' called by sub MainChordsForKey
    ' Globals used: gKeyIndex [index(G0)],
    '       gKeyMode [1=MAJOR,2=MINOR,...],
    '       gScaleDeg [1-7]
    ' Globals set: gRomNumName =, gQuality =,
    '       gTriads(I)

    ' This sub is called for each of the 7
    ' scale degrees from tonic to leadingtone.
    ' For each scale degree,
    ' insert chord numbers 1,3,5,7 at the
    ' proper half-step number 0-11, one per
    ' octave for eight octaves A0,B0,C0 to C8,
    ' from KeyIndexA0 to NOTESMAX.
    ' Fill C0-B0 in the first octave so that
    ' the starting point, e.g.
    ' Index1 = fcnToIx("G0")+{0,2,4,5,7,9,11}
    ' falls before octaves C1-C5.

    ' Enter with gKeyIndex, the half-step index
    ' to the lowest note of the Key, e.g. "G#0",
    ' in gNoteName, and then flag the proper
    ' slots in gTriads with chord numbers 1,3,5,7.
    ' E.g. flag the slots in gTriads of all the
    ' G's with 1, B's with 3, D's with 5, F's with
    ' 7 in the key of G.

    ' In a minor key, after the triads are flagged,
    ' insert triad numbers for the #^6 and #^7
    ' scale degrees.  E.g. if the #^6 occurs a half
    ' step above the 3rd of D-F-A, the iv triad of
    ' the key of A minor, then make the #^6 equal
    ' to a THIRD = 3, to indicate IV D-F#-A.

    ' Per Ottman, don't raise the 5th (#^6) of
    ' iio7 p 283, since it leads to a doubled
    ' leading tone.

    ' C C# D D# E F F# G G# A A# B  C  CHROMATICS
    ' 0 1  2 3  4 5 6  7 8  9 10 11 12 HALF STEPS
    ' 1    2    3 4    5    6    7  8/1 CHORD STEPS
    '^1   ^2   ^3^4   ^5   ^6   ^7 ^8 SCALE DEGREES
                            ' in the key of C Major.

    ' A A# B C  C# D D# E F  F# G  G# A   CHROMATICS
    ' 0 1  2 3  4  5 6  7 8  9  10 11 12  HALF STEPS
    ' 1    2 3     4    5 6     7     8/1 CHORD STEPS
    ' 1   ^2^3    ^4   ^5^6    ^7    ^8   SCALE DEGREES
                            ' in the key of A Minor.
    ' Note #^6 = F# = half step 9  in A minor,
    ' Note #^7 = G# = half step 11 in A minor.

    Const RAISED6TH As Integer = 9
    Const RAISED7TH As Integer = 11
    Dim bRaisedSixth As Boolean
    Dim bRaisedSeventh As Boolean
    Dim I As Integer
    Dim Index1 As Integer
    Dim Incr As Integer
    Dim Incr3rd As Integer
    Dim Incr5th As Integer
    Dim INCRDIM7TH As Integer
    Dim Incr7th As Integer
    Dim INCRMAJ7TH As Integer
    Dim KeyIndexA0 As Integer

'*********************************************
'*********************************************
Main:
    GoSub Init
    GoSub GetQuality
    GoSub MainLine
    Exit Sub

'*********************************************
'*********************************************

Init:
    For I = NOTESMIN To NOTESMAX ' Clear the array
        gTriads(I) = 0
    Next I
    gRomNumName = _
        gScale(gKeyMode).RNumName(gScaleDeg)
    Return

GetQuality:
    gQuality = gScale(gKeyMode).Qual(gScaleDeg)
    Select Case gQuality
    Case QUALMAJOR
        Incr3rd = 4
        Incr5th = 7
        Incr7th = 10 ' Dominant
        INCRMAJ7TH = 11 ' Tonic, Subdominant
    Case QUALMINOR
        Incr3rd = 3
        Incr5th = 7
        Incr7th = 10 ' Supertonic, Mediant, Submediant
    Case QUALDIM ' not used in major, minor
        Incr3rd = 3
        Incr5th = 6
        ' half-dim 7th: B D F A
        Incr7th = 10 ' Leadingtone
    Case QUALDIMDIM
        Incr3rd = 3
        Incr5th = 6
         ' dim 7th chord: B D F Ab
        INCRDIM7TH = 9 ' Leadingtone with flatted 7th
    Case QUALAUGM
        Incr3rd = 4
        Incr5th = 8
        INCRMAJ7TH = 11 ' very rare
    Case Else
        Debug.Print _
            "InitTriads: invalid gQuality="; _
            gQuality
        Stop
    End Select
    Return

    ' Remember:
    '   gKeyName e.g. Cb, C, C#
    '   gKeyNote = gKeyName & "0"
    ' gKeyIndex = fcnToIx(gKeyNote)
MainLine:
    KeyIndexA0 = fcnToIx("A0")

    ' The diatonic chord numbers 1,3,5 are
    ' from the chord numbers 1-8 of an octave.

    Index1 = gKeyIndex + _
        gScale(gKeyMode).HalfStep(gScaleDeg)
    If Index1 > KeyIndexA0 Then
        Index1 = Index1 - 12
    End If
    If Index1 > KeyIndexA0 Then
        Index1 = Index1 - 12 ' only for "B0"
    End If

    For I = Index1 To NOTESMAX Step 12
        If I >= KeyIndexA0 Then
            gTriads(I) = FIRST ' 1 of chord
        End If
    Next I
    For I = Index1 + Incr3rd To NOTESMAX Step 12
        If I >= KeyIndexA0 Then
            gTriads(I) = THIRD ' 3
        End If
    Next I
    For I = Index1 + Incr5th To NOTESMAX Step 12
        If I >= KeyIndexA0 Then
            gTriads(I) = FIFTH ' 5
        End If
    Next I
    
    ' Supertonic 7th ii7 or Dominant 7th V7:
    If gScaleDeg = DOMINANT _
    And (gQuality = QUALMAJOR _
    Or gQuality = QUALMAJOR) Then
        Incr = Incr7th
    ElseIf gScaleDeg = SUPERTONIC _
    And gQuality = QUALMINOR Then
        Incr = Incr7th
    End If
    Return
    
    ' TODO check for modes:
    ' Supertonic 7th ii7 or Dominant 7th V7:
    'If gScaleDeg = 2 Or gScaleDeg = 5 Then
    ' Major 7th= M7 e.g. I7 or IV7
    If gScaleDeg = TONIC _
    Or gScaleDeg = SUBDOMINANT _
    Or gQuality = QUALAUGM Then
        For I = Index1 + INCRMAJ7TH _
            To NOTESMAX Step 12
            If I >= KeyIndexA0 Then
                gTriads(I) = SEVENTH ' 7
            End If
        Next I
    ' Dominant 7th = 7 e.g. ii7, iii7, V7, vi7
    ElseIf gScaleDeg = DOMINANT _
    Or gQuality = QUALMINOR _
    Or gQuality = QUALDIM Then
        For I = Index1 + Incr7th _
            To NOTESMAX Step 12
            If I >= KeyIndexA0 Then
                gTriads(I) = SEVENTH ' 7
            End If
        Next I
    ' Diminished 7th = d7 e.g. viio65
    ElseIf gQuality = QUALDIMDIM Then
        For I = Index1 + INCRDIM7TH _
            To NOTESMAX Step 12
            If I >= KeyIndexA0 Then
                gTriads(I) = SEVENTH ' 7
            End If
        Next I
    End If
    
Increment:
    For I = Index1 + Incr _
        To NOTESMAX Step 12
        If I >= KeyIndexA0 Then
            gTriads(I) = SEVENTH ' 7
        End If
    Next I
    Return

RaisedStep:
    bRaisedSixth = False
    bRaisedSeventh = False
    'If gKeyMode <> MAJOR Then
    If gKeyMode = MINORMELUP Then
        For I = gKeyIndex + RAISED6TH To NOTESMAX _
        Step 12
            If gTriads(I - 1) >= FIRST _
            And gTriads(I - 1) <= FIFTH Then
                gTriads(I) = gTriads(I - 1)
                bRaisedSixth = True
            End If
        Next I
        For I = gKeyIndex + RAISED7TH To NOTESMAX _
        Step 12
            If gTriads(I - 1) >= FIRST _
            And gTriads(I - 1) <= FIFTH Then
                gTriads(I) = gTriads(I - 1)
                bRaisedSeventh = True
            End If
        Next I
    End If
   'If bRaisedSixth Then
   '    gRomNumName = _
   '        gScale(MINORHARM).RNumName(gScaleDeg)
   'End If
   'If bRaisedSeventh Then
   '    gRomNumName = _
   '        gScale(MINORMELUP).RNumName(gScaleDeg)
   'End If
   'If gScaleDeg = 2 Then
   '    Stop
   'End If
   Return
End Sub


Private Sub ScanTriads()
    ' called by sub MainChordsForKey

    ' calls function fcnbBump
    ' calls function fcnGreater
    ' calls function fcnbValidChord
    ' calls sub EditChord
    ' calls sub Drop

    ' Globals used: gB1-2, gT1-2, gA1-2, gS1-2,
    '           gTriads()

    ' Scan the array gTriads for notes, e.g.
    ' for triad G B D, 1=G, 3=B, 5=D. 7=F.
    Dim s As Integer
    Dim A As Integer
    Dim T As Integer
    Dim B As Integer
    Dim I As Integer
    Dim bLower As Boolean
    Dim TotChecked As Integer
    Dim bMsg As Boolean
    ' pbMsg=False is set by sub MakeChordFromInputNotes
    ' to prevent expected error messages
    
    bMsg = True ' True means to print error messages
    bLower = True ' build chords in the Lower Chord Table
    For B = gB1 To gB2 ' range of human bass voice
        If gTriads(B) = 0 Then GoTo NEXTB
        If Not fcnbBump(B) Then GoTo NEXTB ' call
        For T = fcnGreater(B, gT1) To gT2 ' call
            If gTriads(T) = 0 Then GoTo NEXTT
            If Not fcnbBump(T) Then GoTo NEXTT
            For A = fcnGreater(T, gA1) To gA2
                If gTriads(A) = 0 Then GoTo NEXTA
                If Not fcnbBump(A) Then GoTo NEXTA
                For s = fcnGreater(A, gS1) To gS2
                    'GoSub Debug1
                    If gTriads(s) = 0 Then GoTo NEXTS
                    'GoSub Debug1
                    If Not fcnbBump(s) Then GoTo NEXTS
                    TotChecked = TotChecked + 1
                    If A > T + MAXVOICESPREAD _
                    Or s > A + MAXVOICESPREAD Then ' 12
                        ' not over an octave apart
                    ElseIf fcnbValidChord( _
                        B, T, A, s, bMsg) Then ' call
                        Call EditChord(B, T, A, s, bLower)
                    End If
                    Call Drop(gTriads(s))
NEXTS:          Next s
                Call Drop(gTriads(A))

NEXTA:      Next A
            Call Drop(gTriads(T))

NEXTT:  Next T
        Call Drop(gTriads(B))
NEXTB:
    Next B
    If gbDebugPrint Then
        Debug.Print "Total chords checked="; _
            TotChecked; _
            "Scale degree="; gScaleDeg
    End If
    gTotChordsChecked = gTotChordsChecked + TotChecked
    Exit Sub

Debug1:
    If gNoteName(B) = "E3" _
    And gNoteName(T) = "C4" _
    And gNoteName(A) = "G4" _
    And gNoteName(s) = "E5" Then
        GoSub Debug2
        I = I
    End If
Return
    'If gNoteName(B) = "F2" _
    'And gNoteName(T) = "F3" _
    'And gNoteName(A) = "C4" _
    'And gNoteName(S) = "A4" Then

Debug2:
        'Debug.Print
        Debug.Print gNoteName(B); gNoteName(T); _
            gNoteName(A); gNoteName(s); _
            gTriads(B); gTriads(T); _
            gTriads(A); gTriads(s)
        I = I
Return

End Sub

Private Function fcnbBump(pIndex As Integer) As Boolean
    ' called by sub ScanTriads

    ' Globals used: gTriads(pIndex), gScaleDeg
    ' Globals set: gNumOfTriadNotes(TriadNote) =,
    '       gDoubled =, gTriadNoteIndex()=

    ' If the passed TriadNote does not exceed the
    ' numbers allowed for an acceptable
    ' 4-part harmony, add one to that slot
    ' in the gNumOfTriadNotes table, and pass back
    ' Bump=true.
    ' Else pass back Bump=false.

    ' Sub Drop and fcnbBump maintain the counts in the
    ' gNumOfTriadNotes array.

    ' At a final cadence, a triple root and a 3rd
    ' is allowed.
    ' Only one note of a triad can be doubled,
    ' a root, 3rd or 5th.
    ' Other filters apply later.
    
    ' TODO - In fcnbBump, in a major-minor seventh
    ' chord (V7), allow the root to be doubled,
    ' and the fifth omitted.  Ottman p 421.
    
    Dim TriadNote As Integer
    Dim I As Integer ' testing only

    TriadNote = gTriads(pIndex) ' 1,3,5,7
    ' gNumOfTriadNotes numbers 1 3 5, e.g. G B D G
    Select Case TriadNote
        Case FIRST, THIRD, FIFTH, SEVENTH
            '
        Case Else
            Debug.Print "fcnbBump: Invalid chord "; _
                "number: "; TriadNote
        Stop
    End Select

    fcnbBump = False
    ' TriadNote = {1,3,5,7}
    If gNumOfTriadNotes(TriadNote) = 0 Then
        gNumOfTriadNotes(TriadNote) = 1
        gTriadNoteIndex(TriadNote) = pIndex
        fcnbBump = True
    ElseIf gDoubled = 0 Then
        If gNumOfTriadNotes(TriadNote) = 1 Then
            If gNeverDouble( _
                gTriadNoteIndex(TriadNote)) = 0 _
            And gNeverDouble(pIndex) = 0 Then
                gNumOfTriadNotes(TriadNote) = 2
                gDoubled = TriadNote
                gDoubledIndex = pIndex ' use in
                ' fcnbValidChord
                fcnbBump = True
            Else
                I = I
            End If
        End If
    ElseIf TriadNote = gDoubled Then
        ' TODO 12/25/6
        If TriadNote = FIRST Then
        'And gScaleDeg = TONIC Then ' triple root
            If gNumOfTriadNotes(TriadNote) < 3 Then
                gNumOfTriadNotes(TriadNote) = 3
                fcnbBump = True
            End If
        End If
    End If
End Function

Private Function fcnMinArray(pBeg As Integer, _
                            pEnd As Integer, _
                            pArray() As Integer)
    ' called by sub DisplayHarm
    ' Return the smallest integer in pArray
    ' and replace it with max integer = maxinteger.
    Dim I As Integer
    Dim MIN As Integer
    Dim MinIx As Integer
    MIN = 2 ^ 15 - 1
    For I = pBeg To pEnd
        If pArray(I) < MIN Then
            MIN = pArray(I)
            MinIx = I
        End If
    Next I
    pArray(MinIx) = 2 ^ 15 - 1
    fcnMinArray = MIN
End Function



Private Function fcnGreater(pNum1 As Integer, _
                            pNum2 As Integer)
    ' called by sub ScanTriads
    If pNum1 >= pNum2 Then
        fcnGreater = pNum1
    Else
        fcnGreater = pNum2
    End If
End Function
Private Function fcnLesser(pNum1 As Integer, _
                            pNum2 As Integer)
    ' called by sub StoreNotes
    If pNum1 <= pNum2 Then
        fcnLesser = pNum1
    Else
        fcnLesser = pNum2
    End If
End Function

Private Function fcnbValidChord(pBas As Integer, _
                pTen As Integer, _
                pAlt As Integer, _
                pSop As Integer, _
                pbMsg As Boolean) _
                As Boolean
    ' called by sub ScanTriads
    ' called by sub MakeChordFromInputNotes
    ' calls

    ' Globals used: gNeverDouble(),gPrimaryTones(),
    '       gTriads(), gScaleDeg
    ' Globals set: [many booleans],
    '       gInversion =, gChordMode

    ' Returns True=-1 if this is a valid 4-part
    ' chord, else returns False=0.

    ' At a final cadence, a triple root and a third
    ' is allowed, if root pos is the tonic. Ottman.
    ' 12/20/6 Bach example extended to first chord.
    ' Allow any triple with error msg (Triple per
    ' Bach examples.
    
    ' Parms pTen and pAlt are not used.
    
    ' 12/19/6 From Bach chorale examples, I allow:
    ' 1. triple root and 3rd at first chord (Bach 00360b);
    ' 2. double the 3rd in root position (several Bach);


    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim A$
    Dim B$
    Dim B1$
    Dim B2$
    Dim bErr As Boolean
    Dim BassChordNum As Integer ' chord num 1,3,5,7 in
    ' the Bass is used to indicate the inversion.
    Dim bGoodIstInv As Boolean
    
'**********************************************
'**********************************************

Main:
    GoSub Debug7
    If Not bErr Then
        'GoSub Debug5
        GoSub Init
        'GoSub TestChord ' do this only in that song
        GoSub MainLine
    End If
    Exit Function
    
'**********************************************

' If two voices differ by 1,
' e.g. pBas=51 and pTen=52, then
' test at Debug7B.

Debug7:
    If Abs(pBas Mod 12 - pTen Mod 12) = 1 Then
        I = pBas: J = pTen: GoTo Debug7B
    ElseIf Abs(pBas Mod 12 - pAlt Mod 12) = 1 Then
        I = pBas: J = pAlt: GoTo Debug7B
    ElseIf Abs(pBas Mod 12 - pSop Mod 12) = 1 Then
        I = pBas: J = pSop: GoTo Debug7B
    ElseIf Abs(pTen Mod 12 - pAlt Mod 12) = 1 Then
        I = pTen: J = pAlt: GoTo Debug7B
    ElseIf Abs(pTen Mod 12 - pSop Mod 12) = 1 Then
        I = pTen: J = pSop: GoTo Debug7B
    ElseIf Abs(pAlt Mod 12 - pSop Mod 12) = 1 Then
        I = pAlt: J = pSop: GoTo Debug7B
    Else
        Return
    End If
        
' Get the note name of I and J, e.g. Eb4,
' and find the letter in it.

' The the two voices differ by 1  but have
' the same letter name, then
' the generated chord includes a sharp or flat
' and a natural for the same note letter,
' e.g. Eb4 and =E4 or E#4 and =E4,
' so reject this as an invalid chord.
Debug7B:
    A$ = gNoteName(I)
    GoSub Debug7C
    B1$ = B$
    A$ = gNoteName(J)
    GoSub Debug7C
    B2$ = B$
    If B1$ <> B2$ Then
        bErr = False
    Else
        bErr = True
        Debug.Print "fcnbValidChord D7:"; _
            pBas; gNoteName(pBas); " "; _
            pTen; gNoteName(pTen); " "; _
            pAlt; gNoteName(pAlt); " "; _
            pSop; gNoteName(pSop)
        If Abs(I - J) = 11 Then
            I = I
        End If
    End If
    Return

' Find the letter name of this note
Debug7C:
    B$ = ""
    For K = 1 To Len(A$)
        B$ = Mid$(A$, K, 1)
        If B$ >= "A" And B$ <= "G" Then
            Return
        End If
    Next K
    Return
    
'**********************************************
Init:
    bGoodIstInv = False
    fcnbValidChord = False

    gbRootDbl = False
    gbThirdDbl = False
    gbFifthDbl = False
    'gbFinalChord = False
    gbPrimaryTone = False
    gbSopranoDbl = False
    gbDimThirdDbl = False
    gbDimFifthDbl = False
    gbMajor7th = False
    gbDominant7th = False
    gbHalfDim7th = False
    gbDimDim7th = False
    gbSeventh = False
    gb7thComplete = False
    
    '12/20/6
    gbTonicTriple = False
    gbTripleRoot = False
    gbTwoPairs = False
    
    gAlteredChord = 0 ' =6 if some voice is altered
    ' from ^6 to #^6, that is Melodic up.
    ' =7 if some voice is altered
    ' from ^7 to #^7, that is Harmonic.
    ' =3 if some voice is altered
    ' from ^3 to #^3, that is a Picardy third.
    ' May be used to match an altered input note e.g. G#4
    ' or an altered FB: note e.g. #^3 or #6 with
    ' an altered chord.
    
    'GoSub Debug1
    
    ' gDoubledIndex = Index of fcnbBump(Index).
    If gNeverDouble(gDoubledIndex) > 0 Then
        Exit Function
    End If
    If gPrimaryTones(gDoubledIndex) > 0 Then
        gbPrimaryTone = True
    End If
    Return

'**********************************************

' Trap note 7 in Bach's 002506b.txt
TestChord:
    If pBas = fcnToIx("A3") Then '45
        If pBas = 99 Then
            Stop
        End If
        If pTen = fcnToIx("F#4") Then '54
            If pTen = 99 Then
                Stop
            End If
            If fcnToIx("A4") = pAlt Then '57
                If pAlt = 99 Then
                    Stop
                End If
                If fcnToIx("C5") = pSop Then '60
                    If pSop = 99 Then
                        Stop
                    End If
                    If gScaleDeg = LEADINGTONE Then
                        I = I
                    End If
                End If
            End If
        End If
    End If
    Return
    
'**********************************************
    
MainLine:
    GoSub GetQuality
    GoSub GetInversion
    ' gNumOfTriadNotes is increased by fcnbBump
    ' and decreased by Sub Drop:
    If gNumOfTriadNotes(SEVENTH) >= 1 Then
        gbSeventh = True
        GoSub CheckSeventh
    Else
        GoSub CheckBass
    End If
    Return


'**********************************************

GetQuality:
    gChordMode = gKeyMode
    Select Case gKeyMode
        Case MINORNAT, MINORHARM, MINORMELUP
            If gNeverDouble(pBas) = 7 _
            Or gNeverDouble(pTen) = 7 _
            Or gNeverDouble(pAlt) = 7 _
            Or gNeverDouble(pSop) = 7 Then
                gChordMode = MINORHARM
                gAlteredChord = 7
            ElseIf gNeverDouble(pBas) = 6 _
            Or gNeverDouble(pTen) = 6 _
            Or gNeverDouble(pAlt) = 6 _
            Or gNeverDouble(pSop) = 6 Then
                gChordMode = MINORMELUP
                gAlteredChord = 6
            ElseIf gNeverDouble(pBas) = 3 _
            Or gNeverDouble(pTen) = 3 _
            Or gNeverDouble(pAlt) = 3 _
            Or gNeverDouble(pSop) = 3 Then
                gChordMode = MAJOR
                gAlteredChord = 3
            End If
        Case Else
    End Select
    ' override the values set in InitTriads
    gRomNumName = _
        gScale(gChordMode).RNumName(gScaleDeg)
    gQuality = gScale(gChordMode).Qual(gScaleDeg)
    Return
    'Print gTriads(pBas); gTriads(pTen); _
        gTriads(pAlt); gTriads(pSop)

'**********************************************

GetInversion:
    BassChordNum = gTriads(pBas) ' chord num 1,3,5,7 in
    ' the bass; used to indicate the inversion.
    Select Case BassChordNum
        Case FIRST ' chord num
            gInversion = ROOTPOS
        Case THIRD ' chord num
            gInversion = FIRSTINV
        Case FIFTH ' chord num
            gInversion = SECONDINV
        Case SEVENTH ' chord num
            gInversion = THIRDINV
        Case Else
            GoSub Debug2
            ' temporary fix for extra calls by
            ' sub MakeChordFromInputNotes 6aug08
            BassChordNum = FIRST
            gInversion = ROOTPOS
            I = I
    End Select
    Return

'**********************************************
    
    ' (Case 1) If none of the chords 1, 3 or 5 are
    ' doubled,
    '   gDoubled = 0
    ' this must be a complete 7th chord
    '   gb7thComplete = True
    ' with all four voices present.
    ' No further checking of doubling is needed.
    
    ' (Case 2)
    ' In the major-minor seventh chord,
    ' with the root in bass, the root is often
    ' doubled and the fifth omitted. Ottman p 421.
    '   gb7thComplete = False
    ' A complete Dominant V7 is often followed by an
    ' incomplete tonic triad (triple root, omit 5th)
    ' or an incomplete V7 by a complete tonic triad.
    ' Ottman p 281.

CheckSeventh:
    GoSub Set7thFlag
    If gbMajorSeventhExcluded Then
        If gbMajor7th Then ' 25aug08
            fcnbValidChord = False
            GoTo CheckSeventhX
        End If
    End If
      
      If gDoubled = 0 Then
          fcnbValidChord = True
          gb7thComplete = True
      Else
          GoSub CheckInComplete
      End If
    'GoSub CheckQualDim7th ' 3aug08
CheckSeventhX:
    Return

'**********************************************

' The Dominant 7th chords are ii7, iii7, V7, vi7
' but we only use ii7 and V7 per elementary Ottman.
' Allow any scale degree to have a seventh chord,
' complete or incomplete,
' for testing Bach chorales. 7aug08
CheckInComplete:
    'If gQuality = QUALMINOR _
    'Or gScaleDeg = DOMINANT Then
    If gbSeventh Then
        If gNumOfTriadNotes(FIRST) = 2 _
        And gNumOfTriadNotes(FIFTH) = 0 _
        And gTriads(pBas) = FIRST Then
            fcnbValidChord = True
            gb7thComplete = False
        End If
    End If
    Return

'**********************************************

Set7thFlag:
    If gQuality = QUALDIM Then
        gbHalfDim7th = True
    ElseIf gQuality = QUALDIMDIM Then
        gbDimDim7th = True
    ElseIf gQuality = QUALMAJOR Then
        If gScaleDeg = TONIC _
        Or gScaleDeg = SUBDOMINANT Then ' I7 or IV7
            gbMajor7th = True
        Else
            gbDominant7th = True
        End If
    ElseIf gQuality = QUALMINOR Then
        If gScaleDeg = MEDIANT _
        Or gScaleDeg = SUBMEDIANT Then ' III7 or VI7
            gbMajor7th = True
        Else
            gbDominant7th = True
        End If
    End If
    If gbMajor7th Or gbDominant7th _
    Or gbHalfDim7th Or gbDimDim7th Then
        I = I
    Else
        'GoSub Debug6
        I = I
    End If
    Return

'**********************************************

Debug6:
    Debug.Print "fcnbValidChord:D4 "; _
        " gSongCurr,gNoteX="; _
        gSongCurr; gNoteX; _
        " gKeyname gkeymode gquality gscaledeg="; _
        gKeyName; gKeyMode; gQuality; gScaleDeg
    Debug.Print "fcnbValidChord:D4 "; _
        pBas; gTriads(pBas); gNoteName(pBas); _
        pTen; gTriads(pTen); gNoteName(pTen); _
        pAlt; gTriads(pAlt); gNoteName(pAlt); _
        pSop; gTriads(pSop); gNoteName(pSop)
    I = I
    Return

'**********************************************
    Return

'**********************************************

' BEGIN - Not used
' Until I learn otherwise, I allow a Half Dim 7th
' or a Dim Dim 7th chord to have any inversion,
' so skip this gosub.
' The doubling logic was taken from that for
' diminished triads, but probably does not apply.
CheckQualDim7th:
    If gQuality = QUALDIM _
    Or gQuality = QUALDIMDIM Then
        If gInversion <> FIRSTINV Then
            Exit Function
        ElseIf gNumOfTriadNotes(FIRST) > 2 Then
            Exit Function
        ElseIf gTriads(pSop) = FIFTH Then
            If gDoubled = FIFTH Then
                fcnbValidChord = True
                gbDimFifthDbl = True
            Else
                Exit Function
            End If
        ElseIf gDoubled = THIRD Then
            fcnbValidChord = True
            gbDimThirdDbl = True
        End If
        Exit Function
    End If
    Return
' END - Not used
    
'**********************************************
    
CheckBass:
    If gNumOfTriadNotes(FIRST) > 2 Then
        If gNumOfTriadNotes(THIRD) = 1 Then
            gbTripleRoot = True
            fcnbValidChord = True
            If gScaleDeg = TONIC Then
                gbTonicTriple = True
            End If
        Else
            fcnbValidChord = False
        End If
        GoTo Return1
    End If
    Select Case BassChordNum
        Case FIRST ' chord num
            GoSub RootInBass
        Case THIRD ' chord num
            GoSub ThirdInBass
        Case FIFTH ' chord num
            GoSub FifthInBass
        Case SEVENTH ' chord num
            'GoTo Return1
        Case Else
            GoSub Debug2
    End Select
    'fcnbValidChord = True
    'Exit Function
Return1:
    Return

'**********************************************

Debug2:
    ' pbMsg=False is set by sub MakeChordFromInputNotes
    ' to prevent expected error messages
    If pbMsg Then
        Debug.Print "fcnbValidChord:D2 "; _
            " gSongCurr,gNoteX="; _
            gSongCurr; gNoteX; _
            "invalid BassChordNum = gTriads(pBas)= "; _
            BassChordNum
        I = I
        'Stop
    End If
    Return
    
'**********************************************

RootInBass:
    'gInversion = ROOTPOS
    Select Case gDoubled
        Case FIFTH ' doubled
            'Exit Function
        Case SEVENTH ' chord num
            'Exit Function
        Case THIRD ' doubled
        ' In a minor triad with the root or third
        ' in the bass, the third is often doubled,
        ' especially when a principal tone. Ottman p420
        ' 12/19/6 Bach examples allow double 3rds
            If gQuality = QUALMAJOR _
            Or gQuality = QUALMINOR Then
                gbThirdDbl = True
                fcnbValidChord = True
            'Else
            '    Exit Function
            End If
        Case FIRST ' doubled
        ' At a final cadence, a triple root and a
        ' third is allowed, in a tonic triad.
            If gbTripleRoot Then
                If gScaleDeg = TONIC Then
                    gbTonicTriple = True
                End If
                'fcnbValidChord = True
            Else
                gbRootDbl = True
                fcnbValidChord = True
            End If
        Case Else
            GoSub Debug3
    End Select
    'fcnbValidChord = True
    Return

'**********************************************

Debug3:
    ' pbMsg=False is set by sub MakeChordFromInputNotes
    ' to prevent expected error messages
    If pbMsg Then
        Debug.Print "fcnbValidChord:D3 "; _
            " gSongCurr,gNoteX="; _
            gSongCurr; gNoteX; _
            "invalid gDoubled = "; gDoubled
        I = I
        'Stop
    End If
    Return
    
'**********************************************

ThirdInBass:
    'gInversion = FIRSTINV
    If gDoubled = THIRD Then
        I = I
    End If
    ' TODO 12/20/6
    'If gNumOfTriadNotes(gDoubled) > 2 Then
    '    GoTo Return3
    'End If
    If gQuality = QUALDIM Then
        GoSub CheckQualDim
        GoTo Return3
    End If
    If gNumOfTriadNotes(FIRST) = 3 Then
        bGoodIstInv = True
        fcnbValidChord = True
        GoTo Return3
    End If
    If gDoubled = gTriads(pSop) Then
        gbSopranoDbl = True
        bGoodIstInv = True
        fcnbValidChord = True
        GoTo Return3
    End If
    ' In a minor triad with the root or third
    ' in the bass, the third is often doubled,
    ' especially when a principal tone.
    ' In first inversion, almost every doubling
    ' is allowed, so I added major triad and
    ' first and fifth doubled, although they
    ' are more rare. E.g. Bach Ottman p184
    If gQuality = QUALMINOR _
    Or gQuality = QUALMAJOR Then
        If gDoubled = THIRD Then
            gbThirdDbl = True
            bGoodIstInv = True
            fcnbValidChord = True
        ElseIf gDoubled = FIRST Then
            gbRootDbl = True
            bGoodIstInv = True
            fcnbValidChord = True
        ElseIf gDoubled = FIFTH Then
            gbFifthDbl = True
            bGoodIstInv = True
            fcnbValidChord = True
        End If
    End If
    If Not bGoodIstInv And Not gbPrimaryTone Then
        'Exit Function
        GoTo Return3
    End If
Return3:
    Return


'**********************************************

' Per Ottman p 421, Diminished Triad is usually
' found in first inversion only.
' Double the third.  However, when the fifth
' is in the soprano, the fifth is usually doubled.
' E.g. Bach's 002506b.txt note 7, at which I change
' the modulation key from C major to G major. 4aug08

' TODO find a way to show and to discourage the case
' for a diminished triad where the fifth is in the
' soprano but the third is doubled, perhaps by
' adjusting the Variance or Rank. 4aug08
CheckQualDim:
    If gQuality = QUALDIM Then
        If gInversion = FIRSTINV Then
            If gDoubled = FIFTH Then
                If gTriads(pSop) = FIFTH Then
                    fcnbValidChord = True
                    gbDimFifthDbl = True
                End If
            ElseIf gDoubled = THIRD Then
                fcnbValidChord = True
                gbDimThirdDbl = True
            End If
        End If
    End If
    Return

'**********************************************
' gInversion = SECONDINV
' If SECONDINV, double the bass, which is the fifth
FifthInBass:
    If gScaleDeg = MEDIANT _
    Or gScaleDeg = SUBMEDIANT Then
        Exit Function ' very rarely used
    ElseIf gDoubled = FIFTH Then
        gbFifthDbl = True
        fcnbValidChord = True
    'Else
    '    Exit Function
    End If
    Return

'**********************************************

Debug5:
    If gTriads(pBas) = 35 Then
        If gTriads(pTen) = 40 Then
            If gTriads(pAlt) = 55 Then
                If gTriads(pSop) = 60 Then
                    GoSub Debug4
                    I = I
                End If
            End If
        End If
    End If
    Return
    
'**********************************************

Debug4:
        Debug.Print
        Debug.Print "fcnbValidChord:D4 "; _
        " gSongCurr,gNoteX="; _
        gSongCurr; gNoteX; _
        " gKeyname gkeymode gquality gscaledeg="; _
        gKeyName; gKeyMode; gQuality; gScaleDeg
        I = I
    Return

'**********************************************

Debug1:
    If gTriads(pBas) = THIRD _
    And gNoteName(pBas) = "E3" _
    And gNoteName(pSop) = "E5" Then
        Debug.Print
        Debug.Print "fcnbValidChord:D1 "; _
        " gSongCurr,gNoteX="; _
        gSongCurr; gNoteX; _
        THIRD; pSop; gTriads(pSop); _
            gNoteName(pBas); gNoteName(pSop)
        I = I
    End If
    Return
    'And gNoteName(pBas) = "E3" Then
    'And gTriads(pSop) = THIRD _

End Function

Private Sub EditChord(pBas As Integer, _
        pTen As Integer, _
        pAlt As Integer, _
        pSop As Integer, _
        pbLower As Boolean)
    
    ' called by sub ScanTriads if pbLower = True
    ' called by sub MakeChordFromInputNotes
    '   if pbLower = False
    
    ' (Calls fcnVoiceEffort("B",B) - not used)
    ' calls function fcnGetFBName

    ' Globals used:
    ' Globals set: gChordLast,[many gChord values],

'*****************************************
    
    ' Build a ChordRec type record in array Chords
    ' using the passed indexes pBas,pTen,pAlt,pSop
    ' and some global variables.
    
    ' Notice that the size of gChord() depends
    ' strongly on the ranges of the four voices, eg
    '   OP: RANGE   SOPRANO G4-A5
    ' and the number of modulation keys in
    '   gsong(gsonglast).key()
    
    ' The physical array
    '   gChord(CHORDSMAX)
    ' is divided logically into a Lower Chord Table
    ' and an Upper Chord Table.
    ' The Lower ranges from
    '   1 to CHORDSMAX - SONGNOTEMAX
    ' where SONGNOTEMAX=300 is the size of gNote().
    ' The Upper ranges from
    '   CHORDSMAX - SONGNOTEMAX + 1 to CHORDSMAX.
    
    ' The Lower Chord Table is used to build all
    ' the valid chords from the long list of
    ' candidates provided by sub ScanTriads
    ' and function fcnbValidChord.
    
    ' The Upper Chord Table is used to build a
    ' chord from a input note's four tones when
    ' that chord is missing from the Lower Chord
    ' Table.
    '   This is intended for cases where
    ' a completed song is being analyzed, not
    ' where my 4Part program is supplying missing
    ' notes.
    '   A completed song may have notes with
    ' four tones or if option
    '   OP: MISC    NHTINPUT    ON
    ' is used, partial notes are filled in from
    ' previous chords.
    ' May be missing from the table due to invalid:
    '   triad or seventh chord for the current key
    '   range of voices
    '   by doubling rules
    '   by harmonic progression constraints: see (PrgAny
    '   crossed voices in this note
    
'*****************************************
    
    Dim Mean As Single ' mean
    Dim SDev As Single ' standard deviation
    ' First valid gChord starts at index=1.
    ' gChord(0) is used for scanning gChord(1-n).
    Dim I As Integer
    Dim bExit As Boolean
    Dim SaveGChordLast As Integer

'*****************************************
'*****************************************

Main:
    GoSub Init
    If pbLower Then
        GoSub MainLine
    Else
        SaveGChordLast = gChordLast
        gChordLast = gChordLastUpper
        GoSub MainlineUpper
        gChordLast = SaveGChordLast
    End If
    Exit Sub

'*****************************************
'*****************************************

Init:
    bExit = False
    If gAlteredChord = 3 Then
        I = I
    End If
    If pbLower Then
        If gChordLast < LOWERCHORDSMAX Then
            gChordLast = gChordLast + 1
            GoSub CheckReInitialize
        Else
            Debug.Print "EditChord: reached "; _
                "LOWERCHORDSMAX="; LOWERCHORDSMAX
            I = I
            Stop
        End If
        If gSongCurr = 1 Then
            If gNoteX = 101 Then
                If gChordLast = 1818 Then
                    I = I
                End If
            End If
        End If
    Else ' pbLower = False
        If gChordLastUpper < UPPERCHORDSMAX Then
            gChordLastUpper = gChordLastUpper + 1
        Else
            Debug.Print "EditChord: reached "; _
                "UPPERCHORDSMAX="; UPPERCHORDSMAX
            I = I
            Stop
        End If
    End If
    Return
    
'*****************************************

' re-initialize with gChord(CHORDSMAX + 1)
' for cmdNextFile
CheckReInitialize:
    If gbFileNameNext Then
        If gChord(gChordLast).bUsed Then
            gChord(gChordLast) _
                = gChord(CHORDSMAX + 1)
        End If
    End If
    gChord(gChordLast).bUsed = True
    Return
    
'*****************************************

MainLine:
        GoSub CopyVoice
        GoSub CopyMisc
        GoSub CopyInversion
        GoSub CopyStats
        GoSub SetDoubleTypes
    Return
    
'*****************************************

MainlineUpper:
        GoSub CopyVoice
        GoSub CopyMisc
        GoSub CopyInversion
        GoSub CopyStats
        GoSub SetDoubleTypes
    Return
    
'*****************************************

CopyVoice:
    With gChord(gChordLast)
        .Voice(BASS).Index2Na = pBas
        .Voice(TENOR).Index2Na = pTen
        .Voice(ALTO).Index2Na = pAlt
        .Voice(SOPRANO).Index2Na = pSop

        .Voice(BASS).ChordPos = gTriads(pBas)
        .Voice(TENOR).ChordPos = gTriads(pTen)
        .Voice(ALTO).ChordPos = gTriads(pAlt)
        .Voice(SOPRANO).ChordPos = gTriads(pSop)

        .Voice(BASS).RaisedDegree = gNeverDouble(pBas)
        .Voice(TENOR).RaisedDegree = gNeverDouble(pTen)
        .Voice(ALTO).RaisedDegree = gNeverDouble(pAlt)
        .Voice(SOPRANO).RaisedDegree = gNeverDouble(pSop)

       '.Voice(BASS).Effort = _
       '    fcnVoiceEffort("B", pBas) ' call
       '.Voice(TENOR).Effort = _
       '    fcnVoiceEffort("T", pTen)
       '.Voice(ALTO).Effort = _
       '    fcnVoiceEffort("A", pAlt)
       '.Voice(SOPRANO).Effort = _
       '    fcnVoiceEffort("S", pSop)
        Return
        
'*****************************************

CopyMisc:
        .ScaleDeg = gScaleDeg ' index of root of chord
        .ChordMode = gChordMode ' 1=Major, 2=MinorNat,etc
        .AlteredChord = gAlteredChord ' =3 6 7 for #^3,#^6,#^7
        .KeyIndex = gKeyIndex ' index of root of scale
        ' TODO .InBass probably not needed, can
        '       use .Inversion
        .InBass = gTriads(pBas) ' 1,3,5
        .Doubled = gDoubled ' 1,3,5
        If pBas = pTen _
        Or pTen = pAlt _
        Or pAlt = pSop Then
            .bDupePitch = True
        Else
            .bDupePitch = False
        End If
        .RomNumName = gRomNumName ' adjusted in
        ' sub fcnbValidChord for a minor key.
        ' 1-7 scale degree of root of chord
        .Quality = gQuality
            ' 1=Major,2=Minor,3=Dim,4=Augm
        .bPrimaryTone = gbPrimaryTone
        .bSeventh = gbSeventh
        .FBName = fcnGetFBName(gInversion, _
                            gbSeventh) ' call
        .b7thComplete = gb7thComplete
        .bTonicTriple = gbTonicTriple
        .bTripleRoot = gbTripleRoot
        Return

'*****************************************

CopyInversion:
        .Inversion = gInversion
            ' Note: gTriads(pBas) \ 2 = 0,1,2 = inversion
        .xHarmProgFlag = 0
        If .Inversion = FIRSTINV Then
            If .ScaleDeg = MEDIANT Then
                .xHarmProgFlag = .xHarmProgFlag _
                            Or HARMPROG_V_iii6
            ElseIf .ScaleDeg = SUBMEDIANT Then
                .xHarmProgFlag = .xHarmProgFlag _
                            Or HARMPROG_I_vi6
            End If
        End If
        Return
        
'*****************************************

CopyStats:
        .Interval(TMINUSB) = (pTen - pBas) Mod 12
        .Interval(AMINUSB) = (pAlt - pBas) Mod 12
        .Interval(SMINUSB) = (pSop - pBas) Mod 12
        .Interval(AMINUST) = (pAlt - pTen) Mod 12
        .Interval(SMINUST) = (pSop - pTen) Mod 12
        .Interval(SMINUSA) = (pSop - pAlt) Mod 12
        If pSop - pTen < 12 Then
            .bTriadClose = True
        End If
        If pTen - pBas < 12 Then
            .bBassClose = True
        End If
        
        .BassGapCh = pTen - pBas ' gap of tenor and bass
        ' statistics on the Tenor, Alto,
        ' and Soprano indexes into gNoteName:
        '   Mean, Standard Deviation.
        ' For use in guiding melody contours.
        ' Should round to 1 or 2 decimal places.
        Mean = (pTen + pAlt + pSop) / 3
        Mean = Int(Mean * 10 + 0.5) / 10 ' round to 1 decimal
        SDev = Sqr((pTen - Mean) ^ 2 + _
                (pAlt - Mean) ^ 2 + _
                (pSop - Mean) ^ 2)
        SDev = Int(SDev * 10 + 0.5) / 10
        .TriadMean = Mean
        .TriadStdDev = SDev

        Mean = (pBas + pTen + pAlt + pSop) / 4
        Mean = Int(Mean * 10 + 0.5) / 10
        SDev = Sqr((pBas - Mean) ^ 2 + _
                (pTen - Mean) ^ 2 + _
                (pAlt - Mean) ^ 2 + _
                (pSop - Mean) ^ 2)
        SDev = Int(SDev * 10 + 0.5) / 10
        .ChordMean = Mean
        .ChordStdDev = SDev
        Return
        
'*****************************************

SetDoubleTypes:
        If gbSeventh Then
            GoSub DoubleTypesSevenths
            If Not bExit Then
                I = I
            End If
        Else
            GoSub DoubleTypesTriples
            If Not bExit Then
                GoSub DoubleTypesInversion
            End If
        End If
        Return
        
'*****************************************

DoubleTypesSevenths:
        If gbMajor7th Then
            .DblType = Major7th
            bExit = True
        ElseIf gbDominant7th Then
            .DblType = DOMINANT7TH
            bExit = True
        ElseIf gbHalfDim7th Then
            .DblType = HALFDIM7TH
            bExit = True
        ElseIf gbDimDim7th Then
            .DblType = DIMDIM7TH
            bExit = True
        End If
        Return
        
'*****************************************

DoubleTypesTriples:
        If gbTonicTriple And .InBass = 1 Then
            .DblType = FINALTRIPLE
            bExit = True
        ElseIf gbTripleRoot Then
            .DblType = TRIPLEROOT
            bExit = True
        End If
        Return
        
'*****************************************

DoubleTypesInversion:
        Select Case gInversion
        Case ROOTPOS
            If gbRootDbl Then
                .DblType = DBLROOT
            'ElseIf gbTonicTriple Then
            '    .DblType = FINALTRIPLE
            'ElseIf gbTripleRoot Then
            '    .DblType = TRIPLEROOT
            ElseIf gbThirdDbl Then
                If gbPrimaryTone Then
                    .DblType = _
                        DBLTHIRDPRIMRP
                Else
                    .DblType = _
                        DBLTHIRDRP
                End If
            End If
        Case SECONDINV
            If gbFifthDbl Then
                .DblType = DBLFIFTH
            End If
        Case FIRSTINV
            If gbDimFifthDbl Or gbDimThirdDbl Then
                    .DblType = _
                        DBLDIMINISH
            ElseIf gbPrimaryTone Then
                If gbSopranoDbl Then
                    .DblType = _
                        DBLPRIMSOP
                Else
                    .DblType = _
                        DBLPRIMARY
                End If
            ElseIf gbSopranoDbl Then
                .DblType = DBLSOPRANO
            ElseIf gQuality = QUALMINOR Then
                If gbThirdDbl Then
                    If gbPrimaryTone Then
                        .DblType = _
                            DBLPRIMTHIRDMINOR
                    Else
                        .DblType = _
                            DBLTHIRDMINOR
                    End If
                ElseIf gbRootDbl = True Then
                        .DblType = _
                            DBLROOTMINOR
                ElseIf gbFifthDbl = True Then
                        .DblType = _
                            DBLFIFTHMINOR
                End If
            ElseIf gQuality = QUALMAJOR Then
                If gbThirdDbl Then
                        .DblType = _
                            DBLTHIRDMAJOR
                ElseIf gbRootDbl = True Then
                        .DblType = _
                            DBLROOTMAJOR
                ElseIf gbFifthDbl = True Then
                        .DblType = _
                            DBLFIFTHMAJOR
                End If
            End If
        Case Else
            GoSub Err1
        End Select
        Return
        
    End With ' Mainline


'*****************************************
    
Err1:
    Debug.Print "EditChord:E1 "; _
        "invalid gInversion = "; gInversion
    I = I
    'Stop
    Return
    
End Sub

Private Sub MakeChordFromInputNotes(pErr6 As String)
    
    ' called by sub fcnbCandChord
    
    ' calls sub InitTriads
    ' calls function fcnbValidChord
    ' calls sub EditChord

    ' Globals used: gNoteX
    ' Globals set:

    ' See comments in sub EditChord.
    ' Build a ChordRec type record in the
    ' Upper Chord Table in array gChord().
    ' This is to be able to create a (best) candidate
    ' for a note gNoteX that has four input tones
    ' but is missing from the big standard Lower
    ' Chord Table.
    ' May be missing from the table due to invalid:
    '   triad or seventh chord for the current key
    '   range of voices
    '   by doubling rules
    '   by harmonic progression constraints: see (PrgAny
    '   crossed voices in this note
    
    Dim I As Integer
    Dim J As Integer
    Dim K As Integer
    Dim L As Integer
    Dim Count As Integer
    Dim bBool As Boolean
    Dim nUserInput As Integer
    Dim bLower As Boolean
    Dim Voice As Integer
    Dim Idx(4) As Integer
    Dim bErr As Boolean
    Dim bMsg As Boolean
    Dim A$

'*****************************************
'*****************************************

Main:
    GoSub Init
    GoSub MainLine
    Exit Sub

'*****************************************
'*****************************************

Init:
    bLower = False ' build chords in the Upper Chord Table
    'If gNoteX = 33 Then
    If gNoteX = 4 Then
        I = I
    End If
    Return
    
'*****************************************

' Call fcnbValidChord, even though the gNoteX
' is probably an invalid chord,
' in order to set up some
' variables for the chord, like .Romnumname.

MainLine:
    GoSub GetIndexes
    If pErr6 <> "" Then
        GoTo MainLineX
    End If
        ' Remember:
    '   gKeyName e.g. Cb, C, C#
    '   gKeyNote = gKeyName & "0"
    gKeyIndex = fcnToIx(gKeyNote)
    If gKeyIndex = 99 Then
        I = I
    End If
    
    Call InitTriads ' Only once: gScaleDeg
    '   [gKeyIndex, gKeyMode, gScaleDeg]
    If gNoteX = 46 Then
        I = I
    End If
    GoSub Bump
    If pErr6 = "Bump" Then
        GoSub Debug3
    Else
        ' pbMsg=False is set by sub MakeChordFromInputNotes
        ' to prevent expected error messages
        bBool = fcnbValidChord(Idx(4), Idx(3), _
                    Idx(2), Idx(1), bMsg) ' B,T,A,S call
        If Not bBool Then
            I = I
        End If
        GoSub CountTriadNotes
        'GoSub Debug1
    End If
    Call EditChord(Idx(4), Idx(3), Idx(2), _
                Idx(1), bLower) ' B,T,A,S
MainLineX:
    Return
    
'*****************************************
    
CountTriadNotes:
    Count = 0
    For L = 1 To 7
        Count = Count + gNumOfTriadNotes(L)
    Next L
    If Count >= 3 Then
        pErr6 = "Unkn" & Format(Count)
        GoSub Debug3
    Else
        pErr6 = "Unkn"
        GoSub Debug3
    End If
    Return
    
'*****************************************
    
Bump:
    For I = 1 To 7
        gNumOfTriadNotes(I) = 0
        gTriadNoteIndex(I) = 0
    Next I
    gDoubled = 0
    ' above are needed for fcnbBump. 7aug08
    pErr6 = ""
    For I = 4 To 1 Step -1 ' 4 3 2 1 = B T A S
        J = Idx(I) ' .Index2Name
        If J <= 0 Then
            pErr6 = "Bump"
        Else
              K = gTriads(J) ' 1 3 5 or 7
              Select Case K
                  Case 1, 3, 5, 7
                      If Not fcnbBump(J) Then
                          pErr6 = "Bump"
                          Exit For
                      End If
                  Case Else
              End Select
        End If
    Next I
    'GoSub Debug2
    Return
    
'*****************************************
    
Debug2:
    Debug.Print "MakeChordFromInputNotes:D2 "; _
        "gSongCurr,gNoteX="; _
        gSongCurr; gNoteX; _
        " gNumOfTriadNotes()=";
    For L = 1 To 7
        If gNumOfTriadNotes(L) <> 0 Then
            Debug.Print L; gNumOfTriadNotes(L); "/";
        End If
    Next L
    Debug.Print
    Return
    
'*****************************************
    
Debug1:
    Debug.Print "MakeChordFromInputNotes:D1 "; _
        "gSongCurr,gNoteX="; _
        gSongCurr; gNoteX; _
        "Idx()="; Idx(4); Idx(3); Idx(2); Idx(1)
    Return
    
'*****************************************
    
Debug3:
    I = Idx(4)
    J = Idx(3)
    K = Idx(2)
    L = Idx(1)
    If gNote(gNoteX).bCrossedVoices Then
        A$ = " Cross"
    Else
        A$ = " Valid"
    End If
    
    Debug.Print "MakeChordFromInputNotes:D3"; _
        " gSongCurr="; gSongCurr; _
        " gKeyname gKeymode gScaleDeg="; _
        gKeyName; gKeyMode; gScaleDeg; _
        " pErr6="; pErr6; A$; " (bBool)="; bBool; _
        " gNoteX="; gNoteX; gNoteName(I); " "; _
        gNoteName(J); " "; gNoteName(K); " "; gNoteName(L); _
        " gChord="; gChordLastUpper + 1; _
        " gTriads()="; gTriads(Idx(4)); gTriads(Idx(3)); _
        gTriads(Idx(2)); gTriads(Idx(1))
    Return
    
'*****************************************
    
' The number of voices with an input note should
' not be less than four.

' Function fcnNoteIndex will return the
'   .Index2Name
' the index to the tone in gNoteName for this voice.

' If a voice is missing from note GNoteX, then
' function fcnNoteIndex will get the voice from
' a previous (NHF) note, assuming that option
'   OP: MISC    NHTINPUT    ON
' is used.
GetIndexes:
    nUserInput = 0
    For Voice = SOPRANO To BASS
        Idx(Voice) = _
            fcnNoteIndex(gNoteX, Voice, bErr) ' call
        If Idx(Voice) <> 0 Then
            nUserInput = nUserInput + 1
        End If
    Next Voice
    If nUserInput < 4 Then
        pErr6 = "nInput"
    End If
    Return
    
'*****************************************

Err1:
    Debug.Print "MakeChordFromInputNotes:E1"; _
        " gSongCurr,gNoteX,nUserInput="; _
        gSongCurr; _
        gNoteX - gSong(gSongCurr).FirstNote; _
        nUserInput; _
        " has less than four input voices"
    I = I
    Stop
    Return

End Sub
Private Sub ExtractRoot(pRoot$, _
                        pScaledeg As Integer, _
                        pErr4 As String)
    
    ' called by sub fcnbCandChord
    
    ' Globals used: gNoteX
    ' Globals set:

    
    Dim I As Integer
    Dim J As Integer
    Dim Voice As Integer
    Dim nUserInput As Integer
    Dim Idx(4) As Integer
    Dim NoteName$(4)
    Dim NoteLetter$(4)
    Dim Alpha$(7)
    Dim nAlpha(7)
    Dim Chord$
    Dim bErr As Boolean
    Dim A$
    Dim Offset As Integer
    
    Dim MajorDyads As Variant
    Dim MajorTriads As Variant
    Dim MajorSevens As Variant
    Dim SortDyads As Variant
    Dim SortTriads As Variant
    Dim SortSevens As Variant
    Dim Dyads As Variant
    Dim Triads As Variant
    Dim Sevens As Variant
    
'*****************************************

MajorSevens = Array("", "CEGBb", "DF#AC", "EG#BD", _
    "FACEb", "GBDF", "AC#EG", "BD#F#A")
SortSevens = Array("", "BbCEG", "ACDF#", "BDEG#", _
    "ACEbF", "BDFG", "AC#EG", "ABD#F#")
Sevens = Array("", "BCEG", "ACDF", "BDEG", _
    "ACEF", "BDFG", "ACEG", "ABDF")

MajorTriads = Array("", "CEG", "DF#A", "EG#B", _
    "FAC", "GBD", "AC#E", "BD#F#")
SortTriads = Array("", "CEG", "ADF#", "BEG#", _
    "ACF", "BDG", "AC#E", "BD#F#")
Triads = Array("", "CEG", "ADF", "BEG", _
    "ACF", "BDG", "ACE", "BDF")

MajorDyads = Array("", "CE", "CG", "DF#", "DA", _
    "EG#", "GB", "FA", "FC", "GB", "GD", _
    "AC#", "AE", "BD#", "BF#")
SortDyads = Array("", "CE", "CG", "DF#", "AD", _
    "EG#", "BG", "AF", "CF", "BG", "DG", _
    "AC#", "AE", "BD#", "BF#")
Dyads = Array("", "CE", "CG", "DF", "AD", _
    "EG", "BG", "AF", "CF", "BG", "DG", _
    "AC", "AE", "BD", "BF")

'*****************************************
'*****************************************

Main:
    'GoSub Init
    GoSub MainLine
    Exit Sub

'*****************************************
'*****************************************

Init:
    Return
    
'*****************************************

MainLine:
    GoSub GetIndexes
    GoSub ConCatenate
    GoSub MatchChord
    If pErr4 = "" Then
        GoSub GetScaleDeg
    End If
    Return
    
'*****************************************
    
' The number of voices with an input note should
' not be less than four.

' Function fcnNoteIndex will return the
'   .Index2Name
' the index to the tone in gNoteName for this voice.

' If a voice is missing from note GNoteX, then
' function fcnNoteIndex will get the voice from
' a previous (NHF) note, assuming that option
'   OP: MISC    NHTINPUT    ON
' is used.

' TODO consider that I am using gNoteName(index)
' to get the note name instead of the notename
' input by the user.  6aug08
GetIndexes:
    nUserInput = 0
    For Voice = SOPRANO To BASS
        Idx(Voice) = _
            fcnNoteIndex(gNoteX, Voice, bErr) ' call
        If Idx(Voice) > 0 Then
            nUserInput = nUserInput + 1
            NoteName$(Voice) = gNoteName(Idx(Voice))
            GoSub GetLetter
        End If
    Next Voice
    If nUserInput < 4 Then
        GoSub Err1
    End If
    Return
    
'*****************************************

' Get just the letter A from a note name like #A4.
' If a duplicate letter, just overlay it.
' Three unique letters means a triad with a double.
' Four unique letters means a seventh chord.
' Assuming the composer followed the rules.
'
' Sort the letters found in NoteName$ by inserting
' each letter into Alpha$ at its offset from "A".
GetLetter:
    For I = 1 To Len(NoteName$(Voice))
        A$ = Mid$(NoteName$(Voice), I, 1)
        If A$ >= "A" And A$ <= "G" Then
            NoteLetter$(Voice) = A$
            J = Asc(A$) - Asc("A") + 1
            Alpha$(J) = A$
            nAlpha(J) = nAlpha(J) + 1
            Exit For
        End If
    Next I
    Return
    
'*****************************************

' String the non-blank letters found in Alpha$
' into Chord$ in alphabetical order: A B C ...
ConCatenate:
    Chord$ = ""
    For I = 1 To 7
        If Alpha$(I) <> "" Then
            Chord$ = Chord$ & Alpha$(I)
        End If
    Next I
    Return

'*****************************************

' Match the alphabetical ordered Chord$ with
' a Triads or Sevens, each in alphabetical order.
' If a match is found, get the Root letter from
' the Triads or Sevens in standard unsorted order.
MatchChord:
    Offset = 0
    If Len(Chord$) = 2 Then ' triad
        For I = 1 To 14
            If Chord$ = Dyads(I) Then
                Offset = I
                pRoot$ = Left$(MajorDyads(I), 1)
                Exit For
            End If
        Next I
    ElseIf Len(Chord$) = 3 Then ' triad
        For I = 1 To 7
            If Chord$ = Triads(I) Then
                Offset = I
                pRoot$ = Left$(MajorTriads(I), 1)
                Exit For
            End If
        Next I
    ElseIf Len(Chord$) = 4 Then ' seventh
        For I = 1 To 7
            If Chord$ = Sevens(I) Then
                Offset = I
                pRoot$ = Left$(MajorSevens(I), 1)
                Exit For
            End If
        Next I
    Else
        GoSub Err2
    End If
    If Offset = 0 Then
        pErr4 = "None"
    Else
        'GoSub Debug1
        I = I
    End If
    Return
    
'*****************************************
    
' Find the original note name with the Root letter,
' get its .Index2Name, and get its Scaledeg from
' gScaleDeg(.Index2Name).  This is the scaledeg
' of the root of the chord in the input gNoteX.
' Notice that gScaleDegree contains -5 for a TriTone,
' an arbitrary decision of mine.  7aug08
GetScaleDeg:
    pScaledeg = 0
    For J = 1 To 4
        If pRoot$ = NoteLetter$(J) Then
            I = Idx(J) ' .Index2Name
            pScaledeg = Abs(gScaleDegree(I))
            Exit For
        End If
    Next J
    If pScaledeg = 0 Then
        I = I
        Stop
    Else
        'GoSub Debug1
    End If
    Return
    
'*****************************************

Debug1:
    pErr4 = ""
    Debug.Print "ExtractRoot:D1"; _
        " gSongCurr,gNoteX,nUserInput="; _
        gSongCurr; gNoteX; nUserInput; _
        " pRoot$ Chord$ SDeg MajorTriads(I) MajorSevens(I)="; _
        pRoot$; " "; Chord$; pScaledeg; _
        MajorTriads(Offset); " "; MajorSevens(Offset)
    I = I
    Return

    
'*****************************************

Err1:
    pErr4 = "Less4"
    Debug.Print "ExtractRoot:E1"; _
        " gSongCurr,gNoteX,nUserInput="; _
        gSongCurr; gNoteX; nUserInput; _
        " has less than four input voices"
    I = I
    'Stop
    Return

'*****************************************

Err2:
    pErr4 = "Not34"
    Debug.Print "ExtractRoot:E2"; _
        " gSongCurr,gNoteX,nUserInput="; _
        gSongCurr; gNoteX; nUserInput; _
        " does not have 3 or 4 unique input voices"
    I = I
    'Stop
    Return

End Sub





Private Sub Drop(pTriadNote As Integer)
    ' called by sub ScanTriads
    ' calls

    ' Globals used:
    ' Globals set: gNumOfTriadNotes(), gDoubled=0,
    '       gDoubledIndex = 0

    ' Subtract one from the pTriadNote slot
    ' in the gNumOfTriadNotes table.

    ' gNumOfTriadNotes numbers 1 3 5, e.g. G B D G
    Select Case pTriadNote
        Case FIRST, THIRD, FIFTH, SEVENTH
            '
        Case Else
            Debug.Print "Drop: Invalid chord "; _
                "number: "; pTriadNote
        Stop
    End Select
    ' TODO cleanup
   'If pTriadNote = FIRST _
   'Or pTriadNote = THIRD _
   'Or pTriadNote = FIFTH _
   'Or pTriadNote = SEVENTH Then
   '    '
   'Else
   '    Debug.Print "Drop: invalid chord number="; _
   '        pTriadNote
   '    Stop
   'End If

    If gNumOfTriadNotes(pTriadNote) > 0 Then
        gNumOfTriadNotes(pTriadNote) = _
            gNumOfTriadNotes(pTriadNote) - 1
        gTriadNoteIndex(pTriadNote) = 0
        If gDoubled = pTriadNote Then
            If gNumOfTriadNotes(pTriadNote) < 2 Then
                gDoubled = 0
                gDoubledIndex = 0
            End If
        End If
    End If

End Sub



Private Sub PrintDblTypes()
    ' called by sub MainChordsForKey
    ' Globals used: gScaleDeg
    ' Globals set:

    ' Print the ChordRecs by DblTypes
    Dim I As Integer
    Dim J As Integer
    Dim TotValid As Integer
    Dim TotValidType As Integer
    Dim LineNo As Integer
    Dim bAllTitles As Boolean
    bAllTitles = True
    bAllTitles = False

    gbDebugPrintSave = gbDebugPrint
    For I = 1 To DBLTYPEMAX
        LineNo = 0
        If gbDebugPrint And bAllTitles Then
            Debug.Print I; _
                gDblNames(I); _
                ": "; gDblDescs(I)
        End If
        TotValidType = 0
        For J = 1 To gChordLast
        With gChord(J)
            If .DblType = I _
            And .ScaleDeg = gScaleDeg Then
                TotValid = TotValid + 1
                TotValidType = TotValidType + 1
                If TotValidType = 1 _
                And gbDebugPrint Then
                    If Not bAllTitles Then
                        Debug.Print I; _
                            gDblNames(I); _
                            ": "; gDblDescs(I)
                    End If
                    Debug.Print "LN  NV  DB "; _
                        "Bass Tenr Alto Sopr"; _
                        Tab(37); _
                        "TB AB SB  AT ST  SA"; _
                        Tab(61); "BX TX "; _
                        "Triad M/SD"; Tab(79); _
                        "Chord M/SD"
                End If
                LineNo = LineNo + 1
                If gbDebugPrint Then
                    Debug.Print LineNo; Tab(5); _
                        .Inversion; _
                        .Doubled; " ";
                    Debug.Print _
                        gNoteName(.Voice(BASS).Index2Na); _
                        .Voice(BASS).ChordPos; _
                        gNoteName(.Voice(TENOR).Index2Na); _
                        .Voice(TENOR).ChordPos; _
                        gNoteName(.Voice(ALTO).Index2Na); _
                        .Voice(ALTO).ChordPos; _
                        gNoteName(.Voice(SOPRANO).Index2Na); _
                        .Voice(SOPRANO).ChordPos;
                    Debug.Print Tab(36); " "; _
                        .Interval(TMINUSB); _
                        .Interval(AMINUSB); _
                        .Interval(SMINUSB); " "; _
                        .Interval(AMINUST); _
                        .Interval(SMINUST); " "; _
                        .Interval(SMINUSA);
                    Debug.Print Tab(60); _
                        fcnbPrtBool(.bBassClose, _
                            " BC", " BO"); " "; _
                        fcnbPrtBool(.bTriadClose, _
                            "TC", "TO"); ' call
                    Debug.Print _
                        .TriadMean; _
                        .TriadStdDev; Tab(78); _
                        .ChordMean; _
                        .ChordStdDev
                End If
            End If
        End With
        Next J
        gDblType(I).TotVal = gDblType(I).TotVal + _
            TotValidType
        gTotValidType = gTotValidType + TotValidType
    Next I
    'gbDebugPrint = True
    If gbDebugPrint Then
        Debug.Print "Total valid chords="; TotValid;
        Debug.Print "Total valid types="; gTotValidType
    End If
    gTotChordsValid = gTotChordsValid + TotValid
    gbDebugPrint = gbDebugPrintSave
Exit Sub

End Sub



Private Function fcnbPrtBool(pBool As Boolean, _
                                YesAns$, _
                                NoAns$) _
                                As String
    ' called by sub PrintDblTypes
    ' called by sub WriteDblTypes
    If pBool Then
        fcnbPrtBool = YesAns$
    Else
        fcnbPrtBool = NoAns$
    End If
End Function



Private Sub SetColWidth()
' This sample is NOT used:
    Dim Counter
    For Counter = 1 To 4 Step 1
        grdTable4P.ColWidth(Counter) = 1300
    Next
End Sub


Private Sub SetRowHeight()
' This sample is NOT used:
    Dim Counter
    For Counter = 0 To 12 Step 1
        grdTable4P.RowHeight(Counter) = 500
    Next

End Sub


Private Sub Temp()
    ' used for editing only
End Sub



Private Sub PrintTest(pNote As Integer)
    ' calls were commented out but are usable:
    ' called by sub ChooseLastChord
    ' called by sub ChoosePrevChord
    Dim I As Integer
    Dim Bc As Integer
    Dim Ch As Integer
    Dim K1 As Integer
    'Bc = gHarm(pNote).BestCand
    Bc = gNote(pNote).BestCand
    Ch = gCand(Bc).Index2Ch
    K1 = gChord(Ch).Voice(1).Index2Na

    With gSong(gSongCurr)
        Debug.Print "song="; gSongCurr; _
            "note="; pNote
        Debug.Print Spc(5); _
            "First/pNote/LastNote="; _
            .FirstNote; pNote; .LastNote
    End With

    With gNote(pNote)
        Debug.Print Spc(5); _
            "First/Best/LastCand="; _
            .FirstCand; Bc; .LastCand
        Debug.Print Spc(5); "CandFlag="; _
            gCand(Bc).xHarmProgFlag
    End With

    With gChord(Ch)
        Debug.Print Spc(5); "gChord ix="; _
            Ch; "name="; gNoteName(K1)
        For I = SOPRANO To BASS
            K1 = gChord(Ch).Voice(I).Index2Na
            Debug.Print gNoteName(K1);
        Next I
        Debug.Print
        Debug.Print Spc(5); "ChordFlag="; _
            .xHarmProgFlag; "ScaleDeg="; .ScaleDeg; _
            "Inv="; .Inversion
    End With
End Sub
Private Sub PrintTest2(pNote As Integer)
    ' calls were commented out but are usable:
    ' called by sub ChooseLastChord
    ' called by sub ChoosePrevChord
    Dim I As Integer
    Dim J As Integer
    Dim Bc As Integer
    Dim Fc As Integer
    Dim Ch As Integer
    Dim K1 As Integer
    Dim bErr As Boolean

    'Bc = gHarm(pNote).BestCand
    Bc = gNote(pNote).BestCand
    'If Bc = 0 Then
    '    Fc = gNote(pNote).FirstCand
    'End If
    Ch = gCand(Bc).Index2Ch
    K1 = gChord(Ch).Voice(1).Index2Na

    With gSong(gSongCurr)
        Debug.Print "song="; gSongCurr; _
            "note="; pNote
        Debug.Print Spc(5); _
            "First/pNote/LastNote="; _
            .FirstNote; pNote; .LastNote
    End With

    With gNote(pNote)
        Debug.Print Spc(5); "SATB indexes=";
        ' TODO cleanup
        For J = 1 To 4
            Debug.Print .Voice(J).Name; _
               .Voice(J).Index2Name; " ";
               'fcnNoteIndex(pNote, J, bErr); " ";
        Next J

       'For J = 1 To 4
       '    Debug.Print .Voice(J).Name; _
       '        .Voice(J).Index2Na; " ";
       'Next J

        Debug.Print
        Debug.Print Spc(5); _
            "First/Best/LastCand="; _
            .FirstCand; Bc; .LastCand
        Debug.Print Spc(5); _
            "F/B/LastCand.PrevBest="; _
            gCand(.FirstCand).PrevBest; _
            gCand(Bc).PrevBest; _
            gCand(.LastCand).PrevBest
        Debug.Print Spc(5); "CandFlag="; _
            gCand(Bc).xHarmProgFlag
    End With

    With gChord(Ch)
        Debug.Print Spc(5); "gChord ix="; Ch;
        Debug.Print Spc(5);
        For I = SOPRANO To BASS
            K1 = gChord(Ch).Voice(I).Index2Na
            Debug.Print gNoteName(K1); "/";
        Next I
        Debug.Print
        Debug.Print Spc(5); "ChordFlag="; _
            .xHarmProgFlag; "ScaleDeg="; .ScaleDeg; _
            "Inv="; .Inversion
    End With
End Sub

Private Sub PrintCand()
    ' called by the user for debugging
    ' when displaying a particular note
    ' in the candidates grid.

    ' (Called by sub ChooseLastChord)
    ' (Called by sub ChoosePrevChord)

    Dim Bc As Integer
    Dim Ch As Integer
    Dim K1 As Integer
    Dim I As Integer

    'Bc = gHarm(gNoteX).BestCand
    Bc = gNote(gNoteX).BestCand
    Ch = gCand(Bc).Index2Ch
    K1 = gChord(Ch).Voice(1).Index2Na

    With gNote(gNoteX)
        Debug.Print "song="; gSongCurr; _
            "note="; gNoteX
        Debug.Print Spc(5); _
            "First/Best/LastCand="; _
            .FirstCand; .BestCand; .LastCand
        Debug.Print Spc(5); "CandFlag="; _
            gCand(.BestCand).xHarmProgFlag
    End With
    With gChord(Ch)
        Debug.Print Spc(5); "gChord ix="; _
            Ch; "name="; gNoteName(K1)
        For I = SOPRANO To BASS
            K1 = gChord(Ch).Voice(I).Index2Na
            Debug.Print gNoteName(K1);
        Next I
        Debug.Print
        Debug.Print Spc(5); "ChordFlag="; _
            .xHarmProgFlag; "ScaleDeg="; .ScaleDeg; _
            "Inv="; .Inversion
    End With
End Sub
Private Function fcnbFindChord(pCh As Integer, _
                            pS$, pA$, pT$, pB$) _
                            As Boolean
    ' called by sub fcnbCandChord

    ' debugging
    'pCh is chord index to gNoteName()

    Dim K As Integer
    Dim I As Integer
    Dim Note$(SOPRANO To BASS)

    Note$(SOPRANO) = pS$
    Note$(ALTO) = pA$
    Note$(TENOR) = pT$
    Note$(BASS) = pB$

    fcnbFindChord = False
    For I = SOPRANO To BASS
        K = gChord(pCh).Voice(I).Index2Na
        If gNoteName(K) <> Note$(I) Then
            Exit Function
        End If
    Next I

    Debug.Print "fcnbFindChord: song no="; _
        gSongCurr; " chord no="; pCh;
    Debug.Print
    Debug.Print Spc(5); "name=";
    For I = SOPRANO To BASS
        K = gChord(pCh).Voice(I).Index2Na
        Debug.Print gNoteName(K); "/";
    Next I
    Debug.Print
    With gChord(pCh)
        Debug.Print Spc(5); _
            "ChordFlag="; .xHarmProgFlag; _
            "ScaleDeg="; .ScaleDeg; _
            "Inv="; .Inversion
    End With

    fcnbFindChord = True
End Function



Private Sub PrintNotes(pSongno As Integer, _
                        pLineOut As Integer, _
                        pFileNo As Integer)
    ' TODO not used
    ' called by sub WriteCsv
    ' Globals used:
    ' Globals set:

    Dim I As Integer
    Dim R As Integer
    Dim Counter As Integer
    Dim E$

    With gSong(pSongno)
        Write #pFileNo, txtSongNum.Text; _
            txtDesc.Text; _
            txtMeter.Text; _
            txtLength.Text;
        grdKey.Col = 0
        For Counter = 0 To .NumKey - 1
            grdKey.Row = Counter
            Write #pFileNo, grdKey.Text;
        Next Counter
        Write #pFileNo,
        For Row = 1 To grdTable4P.Rows - 1
            grdTable4P.Row = Row
            For Col = 0 To _
                grdTable4P.Cols - 1
                grdTable4P.Col = Col
                Write #pFileNo, grdTable4P.Text;
            Next Col
            Write #pFileNo,
            LineOut = LineOut + 1
        Next Row
    End With

    With gSong(pSongno)
        grdTable4P.Col = 0
        For I = .FirstNote To .LastNote
            grdTable4P.Col = grdTable4P.Col + 1
            grdTable4P.Row = 1
            grdTable4P.Text = _
                gNote(I).Voice(SOPRANO).Name
            grdTable4P.Row = 2
            grdTable4P.Text = _
                gNote(I).Voice(ALTO).Name
            grdTable4P.Row = 3
            grdTable4P.Text = _
                gNote(I).Voice(TENOR).Name
            grdTable4P.Row = 4
            grdTable4P.Text = _
                gNote(I).Voice(BASS).Name
            grdTable4P.Row = 5
            A$ = gNote(I).TMName
            grdTable4P.Text = A$
            GoSub Bars
            grdTable4P.Row = 6
            grdTable4P.Text = gNote(I).RNName
            grdTable4P.Row = 7
            grdTable4P.Text = gNote(I).FBName
            If I < .LastNote Then
                grdTable4P.Row = 8
                grdTable4P.Text = "0"
            End If
        Next I
    End With
    End Sub

Private Function fcnRepFirst(pText As String, _
                        pFrom As String, _
                        pTo As String) As String
    ' called by sub StoreVoiceVn
    
    ' Globals used:
    ' Globals set:
    ' Replace the first string pFrom
    ' found in string pText
    ' with string pTo, and return
    ' the changed string in fcnRepFirst.
    
    ' Use INSTR function for efficiency.
    
    Dim I As Integer
    
    I = InStr(pText, pFrom)
    pText = Left$(pText, I - 1) & pTo _
        & Mid$(pText, I + Len(pFrom))
    fcnRepFirst = pText

End Function

Private Function fcnReplace(pText As String, _
                        pFrom As String, _
                        pTo As String) As String
    ' called by Function fcnbCandChord
    ' called by Sub DisplayHarm
    ' called by Sub WriteAbcNotes
    ' called by Sub StoreVoiceVn
    
    ' Globals used:
    ' Globals set:
    ' Replace all strings pFrom in string pText
    ' with string pTo, and return pText.
    
    ' E.g. replace "A" with "AA", without an
    ' infinite loop.
    
    ' Use INSTR function for efficiency.
    
    ' HINT - To reduce a string of duplicate chars,
    ' like blanks, down to one char, use
    ' function fcnRepDupe.
    
    Dim I As Integer
    Dim K As Integer
    Dim A$
    Dim B$
    
    A$ = pText
    I = InStr(A$, pFrom)
    While I > 0
        B$ = A$
        A$ = Left$(B$, I - 1) & pTo _
            & Mid$(B$, I + Len(pFrom))
        K = I + Len(pTo)
        I = InStr(K, A$, pFrom)
    Wend
    fcnReplace = A$

End Function


Private Function fcnRepDupe(pText As String, _
                        pFrom As String) As String
    
    ' called by Function
    ' called by Sub
    
    ' Globals used:
    ' Globals set:
    ' Replace all strings of duplicate pFrom's
    ' in string pText with a single pFrom = To1,
    ' and return pText.
    
    ' E.g. replace all strings of blanks with
    ' a single blank.
    
    ' HINT - To replace a single char with two or
    ' more of the same chars, use fcnReplace.
    
    ' Use INSTR function for efficiency.
    Dim I As Integer
    Dim K As Integer
    Dim To1 As String
    Dim From1 As String
    Dim A$
    Dim B$
    
    A$ = pText
    ' Replace two pFrom's with a single pFrom
    From1 = pFrom & pFrom
    To1 = pFrom
    
    I = InStr(A$, From1)
    While I > 0
        B$ = A$
        A$ = Left$(B$, I - 1) & To1 _
            & Mid$(B$, I + Len(From1))
        K = I
        I = InStr(K, A$, From1)
    Wend
    fcnRepDupe = A$

End Function


Private Function fcnNote2Col(pNote As Integer) _
                            As Integer
    ' TODO not yet used, but can be used by
    ' sub DisplayHarm.
    ' called by sub PrevTypeChord
    ' called by user in debugging.
    fcnNote2Col = pNote + 1 - _
        gSong(gSongCurr).FirstNote
End Function

Private Function fcnMinorProgression(pCh As Integer, _
        pbBasOK As Boolean, pbExtOK As Boolean, _
        pb6thOK As Boolean, pb7thOK As Boolean, _
        pb6th7th As Boolean) As Boolean
    
    ' called by sub fcnbCandChord
    ' Globals used: gAfterX
    '       gChord()...
    '       gbRaise67OneVoice
    ' Globals set: gCandLast =,
    '       gNote(gNoteX).FirstCand =,
    '       gNote(gNoteX).LastCand =
    '       gCand(gCandLast).Index2Ch =

    ' pCh - index to gChord of a possible
    '   candidate chord for current gNote(gNoteX).

    ' Returns True if a valid Minor Key Progression.
    Static bFirstTime As Boolean
    Static MinorProg As Variant
    Static MinorProgAlt As Variant
    Dim bErr As Boolean
    Dim bExit As Boolean
    Dim bBothRootsInBass As Boolean
    Dim I As Integer
    Dim J As Integer
    Dim SDeg As Integer
    Dim FirstNote As Integer
    Dim AfterCand As Integer
    Dim AfterCandX2Ch As Integer
    Dim Cand As Long
    Dim A$
    Dim B$
    Dim C$
    Dim Pair$
    Dim E$
    Dim BeforeVoice As Integer
    Dim AfterVoice As Integer
    Dim Id1 As Integer
    Dim Id2 As Integer
    Dim ScaleDegree As Integer
    Dim bScaleDegreeFound As Boolean
    
    ' Pointers to the second note after gNoteX and
    ' the first note after gAfterX.  Used to build
    ' a note name like "ii-I-V ", where "I" can interrupt
    ' any valid progression in Ottman p. 208 table.
    Dim TwoAfterX As Integer
    Dim TwoAfterCand As Integer
    Dim TwoAfterCandX2Ch As Integer
    Dim TwoPair$

'*********************************************
'*********************************************
Main:
    GoSub Init
    GoSub MainLine
    Exit Function

'*********************************************
'*********************************************

MainLine:
    'If (pCh = 487 Or pCh = 487) _
    'And gNoteX = 26 Then
    If gNoteX = 2 Then
        I = I
    End If
    bExit = False
    fcnMinorProgression = False
    pbBasOK = False
    pbExtOK = True
    pb6thOK = True
    pb7thOK = True
    pb6th7th = False
    GoSub GetAfter
    If Not bExit Then
        GoSub Progressions
    End If
    Return

'*********************************************

' Per Adrian Quince, any Roman numeral can go to itself
' since there is no movement in the root.
Init:
    If Not bFirstTime Then
        bFirstTime = True
        ' Minor key chord progressions
        ' in Ottman's Table Fig 10.4 pg. 208.
        ' Each string must end with a blank!
        '  V-iii  v-III allowed by Ottman p 305
        MinorProg = Array("", "", _
            " ii o-V ii-V ii-vii o  ii o-ii o ii-ii ", _
            " III-ii o III-iv III-VI  III-III ", _
            " iv-i iv-ii o iv-V iv-VII IV-V IV-vii o  iv-iv IV-IV ", _
            " V-i V-VI v-VI  V-V v-v  V-iii  v-III v-i V-I v-I ", _
            " VI-ii o VI-iv VI-V VI-III  VI-VI ", _
            " vii o-i VII-III  vii o-vii o VII-VII ")
            
            '" V-i V-VI v-VI  V-V v-v  v-V V-v V-iii  v-III v-i v-I ",
            '"VI-III-iv ",
        
        ' Extra minor key chord progressions.
        ' Some are used by Bach.
        MinorProgAlt = Array("", "", _
            " ii o-v ", _
            " III-v III+-V ", _
            " ", _
            " V-IV v-iv ", _
            " ", _
            " vii o-i VII-III ")
        
        ' copied just if needed:
        'MinorProg = Array("", "", _
            '"ii o-V ii o-v ii-V ii-vii o ", _
            '"III-ii o III-iv III-v III+-V III-VI ", _
            '"iv-i iv-ii o iv-V iv-VII IV-V IV-vii o ", _
            '"V-i V-iii V-IV V-VI v-i v-III v-iv v-VI ", _
            '"VI-ii o VI-iv VI-V VI-III ", _
            '"vii o-i VII-III ")
            ' "III-ii o III-iv III-VI ", _
            ' "V-i V-VI v-i v-VI ", _
            ' "VI-ii o VI-iv VI-V VI-III-iv ",
    End If
    Return

'*********************************************

' Get the pointer, AfterCandX2Ch, to the chord of the
' best candidate for the note (gAfterX) after
' the current note (gNoteX).  Will use it to
' get the Roman numeral name of the chord
'   gChord(AfterCandX2Ch).RomNumName
' for scanning MinorProg.
GetAfter:
    If gAfterX = 0 Then ' last note in this song
        bExit = True
        fcnMinorProgression = True
    Else
        ' best candidate for this gAfterX note.
        ' Allow the progression if no BestCand yet?
        AfterCand = gNote(gAfterX).BestCand
        If AfterCand <= 0 Then
            bExit = True
            'GoSub Err1
            fcnMinorProgression = False
        Else
            AfterCandX2Ch = gCand(AfterCand).Index2Ch
            If AfterCandX2Ch <= 0 Then
                bExit = True
                GoSub Err3
                fcnMinorProgression = True
            End If
        End If
    End If
    Return

'*********************************************

' Get the pointer, TwoAfterCandX2Ch, to the chord of the
' best candidate for the second note (TwoAfterX) after
' the current note (gNoteX).  Will use it to
' get the Roman numeral name of the chord
'   gChord(TwoAfterCandX2Ch).RomNumName
' for scanning MinorProg.
GetTwoAfter:
    TwoAfterX = fcnNoteAfter(gAfterX)
    If TwoAfterX = 0 Then ' last note in this song
        bExit = True
        fcnMinorProgression = True
    Else
        ' best candidate for this gTwoAfterX note.
        ' Allow the progression if no BestCand yet?
        TwoAfterCand = gNote(TwoAfterX).BestCand
        If TwoAfterCand <= 0 Then
            bExit = True
            'GoSub Err4
            fcnMinorProgression = True
        Else
            TwoAfterCandX2Ch = gCand(TwoAfterCand).Index2Ch
            If TwoAfterCandX2Ch <= 0 Then
                bExit = True
                GoSub Err5
                fcnMinorProgression = True
            End If
        End If
    End If
    Return

'*********************************************

    ' Take the Roman numeral names of the candidates
    ' of the current and next note, e.g. "ii-V ",
    ' and scan for that string in the array MinorProg.
    
    ' Chord i or I may progress to any other.
    ' Chord I or i may interrupt any other valid
    ' progression, e.g. ii-I-V
Progressions:
    ' SDeg must be 1 to 7, to agree with the number
    ' of elements in array MinorProg().
    SDeg = gChord(pCh).ScaleDeg
    If SDeg = 1 Then
        fcnMinorProgression = True
        bExit = True
        GoTo ProgExit
    ElseIf SDeg < 2 Or SDeg > 7 Then
        bExit = True
        GoSub Err2
        GoTo ProgExit
    End If

    ' Repeated triads are allowed if both have
    ' their root in the bass (Ottman pg.421):
    '   A$=B$ and bBothRootsInBass.
    ' There is a check elsewhere for identical
    ' triads, giving them a low rank.
    A$ = gChord(pCh).RomNumName
    B$ = gChord(AfterCandX2Ch).RomNumName
    ' E.g. Pair$ = " ii-V "
    Pair$ = " " & A$ & "-" & B$ & " "
    If InStr(Pair$, "(") Then Stop ' why this test?
    If UCase$(Pair$) = " V-V " Then
        I = I
    End If
    If Pair$ = " V-VI " Then
        I = I
    End If
    'If pCh = 701 Then
    '    I = I
    'End If
    If gChord(pCh).Inversion = 0 _
    And gChord(AfterCandX2Ch).Inversion = 0 Then
        bBothRootsInBass = True
    Else
        bBothRootsInBass = False
    End If

    ' If Pair$ is not found in the Basic Progression
    ' Table, then search for it in the Extended
    ' Progression Table.
    GoSub BasicProgressions
    If pbBasOK = True Then
        pbExtOK = True ' neutral flag
    Else
        GoSub ExtendedProgressions ' sets pbExtOK
    End If
    ' fcnbCandChord took care of ^6 and ^7 altered notes
    'If gNote(gNoteX).bAlterAboveBass = False _
    'And gNote(gNoteX).bAlterSDeg = False Then
        pb6th7th = False
        If UCase$(A$) = "II" _
        Or UCase$(A$) = "IV" Then
            If gNoteX = 2 Then
                I = I
            End If
            pb6th7th = True
            GoSub CheckRaisedOrLowered6th
        End If
        
        If UCase$(A$) = "V" Then
            pb6th7th = True
            GoSub CheckRaisedOrLowered7th
        End If
    'End If 04may08
    
    If pbBasOK And pbExtOK And pb6thOK And pb7thOK Then
        fcnMinorProgression = True
    Else
        fcnMinorProgression = False
    End If
ProgExit:
    Return

'*********************************************

BasicProgressions:
    If gNote(gAfterX).bFinalChord Then
        If Pair$ = " V-I " Then ' allow Picardy final chord
            pbBasOK = True
            GoTo BasicProgExit
        End If
    End If
    If InStr(MinorProg(SDeg), Pair$) > 0 Then
        pbBasOK = True
        '
        'If A$ <> B$ Then
        '    pbBasOK = True
        'ElseIf bBothRootsInBass Then
        '    pbBasOK = True
            'GoSub Debug1
        'End If
       'If Pair$ = " v-V " Then Stop
    'ElseIf gChord(AfterCandX2Ch).ScaleDeg = 1 Then
    ElseIf UCase$(B$) = "I" Then
    ' e.g. look for three chords ii-i-V as if a pair " ii-V "
        GoSub GetTwoAfter
        If Not bExit Then
            C$ = gChord(TwoAfterCandX2Ch).RomNumName
            TwoPair$ = " " & A$ & "-" & C$ & " "
            If InStr(TwoPair$, "(") Then Stop ' why this test?
            If InStr(MinorProg(SDeg), TwoPair$) > 0 Then
                pbBasOK = True
            End If
        End If
    End If
BasicProgExit:
    Return

'*********************************************

ExtendedProgressions:
    If gNote(gAfterX).bFinalChord Then
        If Pair$ = " V-I " Then ' allow Picardy final chord
            pbExtOK = True
            GoTo ExtendedProgExit
        End If
    End If
    If InStr(MinorProgAlt(SDeg), Pair$) > 0 Then
        If A$ <> B$ Then
            pbExtOK = True
        ElseIf bBothRootsInBass Then
            pbExtOK = True
            'GoSub Debug1
        End If
       'If Pair$ = " v-V " Then Stop
    'ElseIf gChord(AfterCandX2Ch).ScaleDeg = 1 Then
    ElseIf UCase$(B$) = "I" Then
    ' e.g. look for three chords ii-i-V as if a pair " ii-V "
        GoSub GetTwoAfter
        If Not bExit Then
            C$ = gChord(TwoAfterCandX2Ch).RomNumName
            TwoPair$ = " " & A$ & "-" & C$ & " "
            If InStr(TwoPair$, "(") Then Stop ' why this test?
            If InStr(MinorProgAlt(SDeg), TwoPair$) > 0 Then
                pbExtOK = True
            End If
        End If
    End If
ExtendedProgExit:
    Return

'*********************************************

    ' The gScaleDegree array is set in sub
    ' InitScaleDegree.  In a particular scale, e.g.
    ' C Major, the tonic is scale degree 1 and the
    ' Leading Tone is scale degree 7.
    ' Given the index of a voice of a chord,
    ' get the scale degree for that note in the array.
    ' Index2Na is index into gNoteName array.
CheckRaisedOrLowered6th:
    'If (pCh = 487 Or pCh = 487) _
    'And gNoteX = 26 Then
    If gNoteX = 2 Then
        I = I
    End If
    pb6thOK = False
    bScaleDegreeFound = False
    For AfterVoice = SOPRANO To BASS
        Id2 = gChord(AfterCandX2Ch).Voice(AfterVoice).Index2Na
        If gNeverDouble(Id2) = 7 Then ' #^7 Leading Tone
            bScaleDegreeFound = True
            GoSub Match6thAscending
            Exit For ' assume only one #^7 in a chord
        End If
    Next AfterVoice
    If bScaleDegreeFound Then
        Return
    End If
    
    For AfterVoice = SOPRANO To BASS
        Id2 = gChord(AfterCandX2Ch).Voice(AfterVoice).Index2Na
        ScaleDegree = gScaleDegree(Id2)
        If ScaleDegree = 5 Then
            bScaleDegreeFound = True
            GoSub Match6thDescending
            If pb7thOK = True Then
                Exit For
            End If
        End If
    Next AfterVoice
    
    If bScaleDegreeFound Then
        Return
    Else ' accept anything if after chord<> ^1 or ^6.
        pb6thOK = True ' 05may08
    End If
    
    Return
     
'*********************************************

Match6thAscending:
    If gbRaise67OneVoice Then
        ' #^6 ascends to #^7 in the SAME AfterVoice
        Id1 = gChord(pCh).Voice(AfterVoice).Index2Na
        If gNeverDouble(Id1) <> 6 Then ' Id1 is not #^6
            pb6thOK = True ' ok if not SDeg=6 for Id1
        Else ' Id1 is #^6
            If Not gbRaise67Close Then
                pb6thOK = True ' any #^6 to #^7
            ElseIf Id2 = Id1 + 2 Then
                pb6thOK = True ' #^6 is close to #^7
            End If
        End If
    Else '#^6 ascends to #^7 in ANY BeforeVoice
        For BeforeVoice = SOPRANO To BASS
            Id1 = gChord(pCh).Voice(BeforeVoice).Index2Na
            If gbRaise67Close Then
                If Id2 = Id1 + 2 Then ' close #^6 to #^7
                    pb6thOK = True
                    Exit For
                End If
            ElseIf gNeverDouble(Id1) = 6 Then ' any #^6 to #^7
                pb6thOK = True
                Exit For
            End If
        Next BeforeVoice
    End If
    Return
                    
'*********************************************

Match6thDescending:
    If gbRaise67OneVoice Then
        ' b^6 descends to ^5 in the SAME AfterVoice
        Id1 = gChord(pCh).Voice(AfterVoice).Index2Na
        If gScaleDegree(Id1) = 6 _
        And gNeverDouble(Id1) <> 6 Then
            If gbRaise67Close Then
                If Id2 = Id1 - 1 Then ' close b^6 to ^5
                    pb6thOK = True
                End If
            ElseIf gNeverDouble(Id1) <> 6 Then ' any b^6 to ^5
                pb6thOK = True
            End If
        End If
    Else ' b^6 descends to ^5 in the ANY Voice
        For BeforeVoice = SOPRANO To BASS
            Id1 = gChord(pCh).Voice(BeforeVoice).Index2Na
            If gScaleDegree(Id1) = 6 _
            And gNeverDouble(Id1) <> 6 Then
                If gbRaise67Close Then
                    If Id2 = Id1 - 1 Then ' close b^6 to ^5
                        pb6thOK = True
                        Exit For
                    End If
                ElseIf gNeverDouble(Id1) <> 6 Then ' any b^6 to ^5
                    pb6thOK = True
                    Exit For
                End If
            End If
        Next BeforeVoice
    End If
    Return
                    
'*********************************************

    ' The gScaleDegree array is set in sub
    ' InitScaleDegree.  In a particular scale, e.g.
    ' C Major, the tonic is scale degree 1 and the
    ' Leading Tone is scale degree 7.
    ' Given the index of a voice of a chord,
    ' get the scale degree for that note in the array.
    ' Index2Na is index into gNoteName array.
CheckRaisedOrLowered7th:
    If (pCh = 636 Or pCh = 633) _
    And gNoteX = 24 Then
        I = I
    End If
    pb7thOK = False
    bScaleDegreeFound = False
    For AfterVoice = SOPRANO To BASS
        Id2 = gChord(AfterCandX2Ch).Voice(AfterVoice).Index2Na
        ScaleDegree = gScaleDegree(Id2)
        If ScaleDegree = 1 Then ' Tonic
            bScaleDegreeFound = True
            GoSub Match7thAscending
            If pb7thOK = True Then
                Exit For
            End If
        End If
    Next AfterVoice
    If bScaleDegreeFound Then
        Return
    End If
    
    For AfterVoice = SOPRANO To BASS
        Id2 = gChord(AfterCandX2Ch).Voice(AfterVoice).Index2Na
        ScaleDegree = gScaleDegree(Id2)
        If ScaleDegree = 6 Then
            bScaleDegreeFound = True
            GoSub Match7thDescending
            If pb7thOK = True Then
                Exit For
            End If
        End If
    Next AfterVoice
    If bScaleDegreeFound Then
        Return
    Else ' accept anything if after chord is not ^1 or ^6.
        pb7thOK = True
    End If
    
    Return
     
'*********************************************

' In a minor key,
' if gbRaise67Close=true, then the leading tone must be
' a half step lower than the tonic.
' Otherwise (default=false), the leading tone can be
' in any octave, as long as the following chord
' contains a tonic note.  Ottman Fig 4.6 pg. 66.

' In a minor key,
' if gbRaise67OneVoice=true, then the leading tone must be
' in the same voice as the tonic note in the chord following.
' Otherwise (default=false), the leading tone and tonic can be
' in any voices.
Match7thAscending:
    If gbRaise67OneVoice Then
        '#^7 ascends to tonic in the SAME AfterVoice
        Id1 = gChord(pCh).Voice(AfterVoice).Index2Na
        If gbRaise67Close Then
            If Id2 = Id1 + 1 Then ' close #^7 to tonic
                pb7thOK = True
            End If
        ElseIf gNeverDouble(Id1) = 7 Then ' any #^7 to tonic
            pb7thOK = True
        End If
    Else '#^7 ascends to tonic in ANY voice
        For BeforeVoice = SOPRANO To BASS
            Id1 = gChord(pCh).Voice(BeforeVoice).Index2Na
            If gbRaise67Close Then
                If Id2 = Id1 + 1 Then ' close #^7 to tonic
                    pb7thOK = True
                    Exit For
                End If
            ElseIf gNeverDouble(Id1) = 7 Then ' any #^7 to tonic
                pb7thOK = True
                Exit For
            End If
        Next BeforeVoice
    End If
    Return
                    
'*********************************************

Match7thDescending:
    If gbRaise67OneVoice Then
        ' b^7 descends to b^6 in the SAME AfterVoice
        Id1 = gChord(pCh).Voice(AfterVoice).Index2Na
        If gScaleDegree(Id1) = 7 _
        And gNeverDouble(Id1) <> 7 Then
            If gbRaise67Close Then
                If Id2 = Id1 - 2 Then ' close b^7 to ^6
                    pb7thOK = True
                End If
            ElseIf gNeverDouble(Id1) <> 6 Then ' any b^7 to ^6
                pb7thOK = True
            End If
        End If
    Else ' b^7 decends to b^6 in ANY voice
        For BeforeVoice = SOPRANO To BASS
            Id1 = gChord(pCh).Voice(BeforeVoice).Index2Na
            If gScaleDegree(Id1) = 7 _
            And gNeverDouble(Id1) <> 7 Then
                If gbRaise67Close Then
                    If Id2 = Id1 - 2 Then ' close b^7 to ^6
                        pb7thOK = True
                        Exit For
                    End If
                ElseIf gNeverDouble(Id1) <> 6 Then ' any b^7 to ^6
                    pb7thOK = True
                    Exit For
                End If
            End If
        Next BeforeVoice
    End If
    Return
                    
'*********************************************

Err1:
    gInputErrs = gInputErrs + 1
    FirstNote = gSong(gSongCurr).FirstNote
    E$ = "fcnMinorProgression:E1 invalid AfterCand=" & _
        AfterCand & " for note gAfterX=" & _
        gAfterX - FirstNote + 1 & _
        " in song=" & gSongCurr & _
        ". Probably NoSol1 in note after gAfterX."
    'txtPrint.Text = E$
    Call DisplayText(E$)
    Debug.Print E$
    Stop
    Return

Err2:
    gInputErrs = gInputErrs + 1
    FirstNote = gSong(gSongCurr).FirstNote
    E$ = "fcnMinorProgression:E2 invalid SDeg (=" & _
        SDeg & " scale degree for note gAfterX=" & _
        gAfterX - FirstNote + 1 & _
        " in song=" & gSongCurr
    'txtPrint.Text = E$
    Call DisplayText(E$)
    Debug.Print E$
    I = I
    Stop
    Return

Err3:
    gInputErrs = gInputErrs + 1
    FirstNote = gSong(gSongCurr).FirstNote
    E$ = "fcnMinorProgression:E3 invalid AfterCandX2Ch=" & _
        AfterCandX2Ch & " for note gAfterX=" & _
        gAfterX - FirstNote + 1 & _
        " in song=" & gSongCurr
    'txtPrint.Text = E$
    Call DisplayText(E$)
    Debug.Print E$
    I = I
    Stop
    Return
    
Err4:
    gInputErrs = gInputErrs + 1
    FirstNote = gSong(gSongCurr).FirstNote
    E$ = "fcnMinorProgression:E4 invalid TwoAfterCand=" & _
        TwoAfterCand & " for note TwoAfterX=" & _
        TwoAfterX - FirstNote + 1 & _
        " in song=" & gSongCurr
    'txtPrint.Text = E$
    Call DisplayText(E$)
    Debug.Print E$
    Stop
    Return

Err5:
    gInputErrs = gInputErrs + 1
    FirstNote = gSong(gSongCurr).FirstNote
    E$ = "fcnMinorProgression:E5 invalid TwoAfterCandX2Ch=" & _
        TwoAfterCandX2Ch & " for note TwoAfterX=" & _
        TwoAfterX - FirstNote + 1 & _
        " in song=" & gSongCurr
    'txtPrint.Text = E$
    Call DisplayText(E$)
    Debug.Print E$
    I = I
    Stop
    Return

Debug1:
    FirstNote = gSong(gSongCurr).FirstNote
    E$ = "fcnMinorProgression:D1 " & _
        "display triads with repeated roots in bass" & _
        "for note TwoAfterX=" & _
        TwoAfterX - FirstNote + 1 & _
        " in song=" & gSongCurr
    'txtPrint.Text = E$
    Call DisplayText(E$)
    Debug.Print E$
    DebugChord (pCh)
    DebugChord (AfterCandX2Ch)
    'Debug.Print
    Return
    
End Function


Private Sub DebugNote(pNote As Integer)
    ' called by sub Harmonize
    ' called by sub ChoosePrevChord
    ' Globals used:
    ' Globals set:

    Dim I As Integer
    Dim Cand As Long
    Dim Chord As Integer
    Dim V As Integer
    Dim Lyric As String

    With gNote(pNote)
        Lyric = ""
        For I = SOPRANO To BASS
            Lyric = .Voice(I).Lyric(1)
            If Lyric <> "" Then
                Exit For
            End If
        Next I
        If Lyric = "" Then
            For I = 1 To 4
                Lyric = .Lyric(I)
                If Lyric <> "" Then
                    Exit For
                End If
            Next I
        End If
    End With
    With gNote(pNote)
        Debug.Print "N="; pNote; "SATB=";
        For I = SOPRANO To BASS
            Debug.Print .Voice(I).Index2Name; _
                .Voice(I).ChordPos; _
                .Voice(I).Name; "/";
        Next I
        Debug.Print "InpCrd="; .ChordInput;
        Debug.Print "Gd="; .nGoodCand;
        Debug.Print "Be="; .BestCand;
        Debug.Print .FirstCand; "-"; .LastCand;
        Debug.Print "So="; .SolutionNo;
        Debug.Print "Meas="; .MeasureNo;
        Debug.Print "Ly="; Lyric
        Cand = .BestCand
        If Cand > 0 Then
            Call DebugCand(Cand)
        End If
    End With
    Exit Sub

' NOT USED
DisplayCand:
    With gCand(Cand)
        If .Error <> "" Then
            Debug.Print .Error; " ";
        End If
        If .Error2 <> "" Then
            Debug.Print "/"; .Error2; " ";
        End If
        Chord = .Index2Ch
        Debug.Print
        Call DebugChord(Chord)
    End With
    Return
End Sub

Private Sub DebugCand(pCand As Long)
    ' called by sub DebugNote
    ' Globals used:
    ' Globals set:

    Dim I As Integer
    Dim Chord As Integer
    Dim bPrint As Boolean

    GoSub DisplayCand
    Exit Sub

DisplayCand:
    With gCand(pCand)
        Debug.Print "Best="; pCand; _
            " Error/Error2="; _
            .Error; " "; .Error2
        Chord = .Index2Ch
        Call DebugChord(Chord)
    End With
    Return
End Sub


Private Sub DebugBadCands(pN As Integer)
    ' called by sub Harmonize
    ' called by sub ChoosePrevChord
    ' Globals used:
    ' Globals set:

    Dim I As Integer
    Dim Can As Integer
    Dim Ch As Integer
    Dim V As Integer

    With gNote(pN)
        Debug.Print "N="; pN;
        Debug.Print "Gd="; .nGoodCand;
        Debug.Print "Be="; .BestCand;
        Debug.Print .FirstCand; "-"; .LastCand;
        Debug.Print "So="; .SolutionNo
        'Can = .BestCand
        For Can = .FirstCand To .LastCand
            GoSub DisplayCand
        Next Can
        Debug.Print
    End With
    Exit Sub


DisplayCand:
    With gCand(Can)
        If .Error <> "" Then
            Debug.Print Can; "|"; .Error; " ";
        End If
        If .Error2 <> "" Then
            Debug.Print "/"; .Error2; " ";
        End If
        'Ch = .Index2Ch
        'Debug.Print
        'call DebugChord(Ch)
    End With
    Return
End Sub


Private Sub FindChord(pS As Integer, _
                    pA As Integer, _
                    pT As Integer, _
                    pB As Integer)
    ' called by hand for debugging
    ' calls sub DebugChord
    
    ' Globals used:
    ' Globals set:

    Dim I As Integer
    Dim bFound As Integer
    Dim Chord As Integer

    For Chord = 1 To gChordLast
        With gChord(Chord)
            If pS = .Voice(SOPRANO).Index2Na _
            And pA = .Voice(ALTO).Index2Na _
            And pT = .Voice(TENOR).Index2Na _
            And pB = .Voice(BASS).Index2Na Then
                Call DebugChord(Chord)
                bFound = True
                Exit For
            End If
        End With
    Next Chord
    
    If Not bFound Then
        Debug.Print "FindChord:D1 no chord found for SATB="; _
            pS; gNoteName(pS); _
            pA; gNoteName(pA); _
            pT; gNoteName(pT); _
            pB; gNoteName(pB)
    End If

End Sub



Private Sub DebugChord(pChord As Integer)
    ' called by sub Harmonize
    ' called by sub ChoosePrevChord
    ' called by sub DebugNote
    ' Globals used:
    ' Globals set:

    Dim I As Integer
    Dim V As Integer
    Dim Pos As Integer

    With gChord(pChord)
        Debug.Print "Ch="; pChord;
        Debug.Print .RomNumName; " "; .FBName;
        Debug.Print " Sd="; .ScaleDeg;
        Debug.Print "Nv="; .Inversion;
        For I = SOPRANO To BASS
            V = .Voice(I).Index2Na
            Pos = .Voice(I).ChordPos
            Debug.Print V; Pos; gNoteName(V); "/";
        Next I
        Debug.Print
    End With
End Sub




Private Sub InterleaveAbcLines(pOFileNo As Integer)

' 17nov08
' called by sub InputLines

' After ABC lines are read in and parsed by sub Parse
' and concatenated into TM, T1-T4, V1-V4 and 11-41 lines,
' plus a comment line counting the notes and measures,
' before they are processed as normal input by
' sub InputLines, use this sub to output the lines
' to file g4PartFile.

' Use option
'   OP: MISC BARSPERLINE 2
' to specify how many measures or bars to output
' per line.  The default is gBarsPerLine = 2.

' Interleave the comment line and the 13 lines,
' as follows:
'   comment (noteno and measno),
'   TM,
'   T1, V1, 11,
'   T2, V2, 21,
'   T3, V3, 31,
'   T4, V4, 41

' The result is that the input ABC lines are output
' in the form of 4Part input lines, stacked
' vertically in short lines.  When I take the
' 4Part lines as input to a new job,
' I can add othe control lines to the stacked
' lines, e.g. RomNum, FixedBass, Error.
' Also I can create new control lines, e.g.
' to specify the kind of passing tones and
' non-harmonic tones.  Also, %%MIDI control
' cards to ABCPlus like volume, instruments, etc.

' The 4Part format output lines may look like this,
' where each field is separated by a TAB or Chr$(9)
' so that they line up vertically.
' %   1   2   |m:2    4   5   |
' TM: e4  e2  |   e4  e2  |
' T1: e4~ e2  |   e4~ e2  |
' V1: C5  A4  |   D4  B4  |
' 11: Look    at  |   you,    just    |
' T2: etc.

'******************************************

Dim I As Integer
Dim K As Integer
Dim J As Integer
Dim Text As String
Dim bFound As Boolean
Dim bTextFound As Boolean
Dim A$
Dim B$
Dim nBars As Integer
Dim nFile As Integer
Dim Pos1 As Integer
Dim Pos2 As Integer
Dim Leng As Integer
Dim V As Integer
Const MAXVAL As Integer = 32767

Dim Voice As Integer

'******************************************

MainLine:
    GoSub Init
    GoSub Main
    Exit Sub

'******************************************

Init:
    bTextFound = True
    gAbcNoteNo.Pos = 0
    gAbcMeasNo.Pos = 0
    gAbcTM.Pos = 0
    For I = SOPRANO To BASS
        gAbcT(I).Pos = 0
        gAbcV(I).Pos = 0
        gAbcI(I).Pos = 0
    Next I
    Return

'******************************************

Main:
    Do While bTextFound
        bTextFound = False
        Text = "%"
        GoSub PutFile
        If gAbcNoteNo.Pos < MAXVAL Then
            GoSub PutNoteNo
        End If
        If gAbcTM.Pos < MAXVAL Then
            GoSub PutAbcTM
        End If
        For V = SOPRANO To BASS
            If gAbcT(V).Pos < MAXVAL Then
                GoSub PutAbcT
            End If
            If gAbcV(V).Pos < MAXVAL Then
                GoSub PutAbcV
            End If
            If gAbcI(V).Pos < MAXVAL Then
                GoSub PutAbcI
            End If
        Next V
    Loop
    Return
    
'******************************************

PutNoteNo:
    nBars = 0
    Pos1 = gAbcNoteNo.Pos
    If Pos1 = 0 Then
        Pos1 = 1
        Text = ""
    Else
        Pos1 = Pos1 + 1
        Text = "%"
    End If
    Pos2 = Pos1
    Leng = Len(gAbcNoteNo.String)
    Do While Pos2 < Leng
        Pos2 = Pos2 + 1
        A$ = Mid$(gAbcNoteNo.String, Pos2, 1)
        If A$ = "|" Then
            nBars = nBars + 1
            If Pos2 < Leng Then ' check double bar
                B$ = Mid$(gAbcNoteNo.String, Pos2 + 1, 1)
                If B$ = "|" Then
                    Pos2 = Pos2 + 1
                End If
            End If
            If nBars >= gBarsPerLine Then
                GoSub PutNoteNo2
                Exit Do
            End If
        End If
    Loop
    
    If Pos2 >= Leng Then
        If nBars < gBarsPerLine Then
            If nBars > 0 Then ' get end of line
                GoSub PutNoteNo2
            End If
        End If
        gAbcNoteNo.Pos = MAXVAL
    End If
    Return
    
PutNoteNo2:
    bTextFound = True
    Text = Text & _
        Mid$(gAbcNoteNo.String, _
        Pos1, Pos2 - Pos1 + 1)
    gAbcNoteNo.Pos = Pos2
    GoSub PutFile
    Return

'******************************************

PutAbcTM:
    nBars = 0
    Pos1 = gAbcTM.Pos
    If Pos1 = 0 Then
        Pos1 = 1
        Text = ""
    Else
        Pos1 = Pos1 + 1
        Text = Mid$(gAbcTM.String, 1, 3) ' TM:
    End If
    Pos2 = Pos1
    Leng = Len(gAbcTM.String)
    Do While Pos2 < Leng
        Pos2 = Pos2 + 1
        A$ = Mid$(gAbcTM.String, Pos2, 1)
        If A$ = "|" Then
            nBars = nBars + 1
            If Pos2 < Leng Then ' check double bar
                B$ = Mid$(gAbcTM.String, Pos2 + 1, 1)
                If B$ = "|" Then
                    Pos2 = Pos2 + 1
                End If
            End If
            If nBars >= gBarsPerLine Then
                GoSub PutAbcTM2
                Exit Do
            End If
        End If
    Loop
    
    If Pos2 >= Leng Then
        If nBars < gBarsPerLine Then
            If nBars > 0 Then ' get end of line
                GoSub PutAbcTM2
            End If
        End If
        gAbcTM.Pos = MAXVAL
    End If
    Return
    
PutAbcTM2:
    bTextFound = True
    Text = Text & _
        Mid$(gAbcTM.String, _
        Pos1, Pos2 - Pos1 + 1)
    gAbcTM.Pos = Pos2
    GoSub PutFile
    Return

'******************************************

PutAbcT:
    Leng = Len(gAbcT(V).String)
    If Leng <= 3 Then
        Return
    End If
    nBars = 0
    Pos1 = gAbcT(V).Pos
    If Pos1 = 0 Then
        Pos1 = 1
        Text = ""
    Else
        Pos1 = Pos1 + 1
        Text = Mid$(gAbcT(V).String, 1, 3) ' T1: etc
    End If
    Pos2 = Pos1
    Do While Pos2 < Leng
        Pos2 = Pos2 + 1
        A$ = Mid$(gAbcT(V).String, Pos2, 1)
        If A$ = "|" Then
            nBars = nBars + 1
            If Pos2 < Leng Then ' check double bar
                B$ = Mid$(gAbcT(V).String, Pos2 + 1, 1)
                If B$ = "|" Then
                    Pos2 = Pos2 + 1
                End If
            End If
            If nBars >= gBarsPerLine Then
                GoSub PutAbcT2
                Exit Do
            End If
        End If
    Loop
    
    If Pos2 >= Leng Then
        If nBars < gBarsPerLine Then
            If nBars > 0 Then ' get end of line
                GoSub PutAbcT2
            End If
        End If
        gAbcT(V).Pos = MAXVAL
    End If
    Return
    
PutAbcT2:
    bTextFound = True
    Text = Text & _
        Mid$(gAbcT(V).String, _
        Pos1, Pos2 - Pos1 + 1)
    gAbcT(V).Pos = Pos2
    GoSub PutFile
    Return

'******************************************

PutAbcV:
    Leng = Len(gAbcV(V).String)
    If Leng <= 3 Then
        Return
    End If
    nBars = 0
    Pos1 = gAbcV(V).Pos
    If Pos1 = 0 Then
        Pos1 = 1
        Text = ""
    Else
        Pos1 = Pos1 + 1
        Text = Mid$(gAbcV(V).String, 1, 3) ' V1: etc
    End If
    Pos2 = Pos1
    Do While Pos2 < Leng
        Pos2 = Pos2 + 1
        A$ = Mid$(gAbcV(V).String, Pos2, 1)
        If A$ = "|" Then
            nBars = nBars + 1
            If Pos2 < Leng Then ' check double bar
                B$ = Mid$(gAbcV(V).String, Pos2 + 1, 1)
                If B$ = "|" Then
                    Pos2 = Pos2 + 1
                End If
            End If
            If nBars >= gBarsPerLine Then
                GoSub PutAbcV2
                Exit Do
            End If
        End If
    Loop
    
    If Pos2 >= Leng Then
        If nBars < gBarsPerLine Then
            If nBars > 0 Then ' get end of line
                GoSub PutAbcV2
            End If
        End If
        gAbcV(V).Pos = MAXVAL
    End If
    Return
    
PutAbcV2:
    bTextFound = True
    Text = Text & _
        Mid$(gAbcV(V).String, _
        Pos1, Pos2 - Pos1 + 1)
    gAbcV(V).Pos = Pos2
    GoSub PutFile
    Return

'******************************************

PutAbcI:
    Leng = Len(gAbcI(V).String)
    If Leng <= 3 Then
        Return
    End If
    nBars = 0
    Pos1 = gAbcI(V).Pos
    If Pos1 = 0 Then
        Pos1 = 1
        Text = ""
    Else
        Pos1 = Pos1 + 1
        Text = Mid$(gAbcI(V).String, 1, 3) ' 11: etc
    End If
    Pos2 = Pos1
    Do While Pos2 < Leng
        Pos2 = Pos2 + 1
        A$ = Mid$(gAbcI(V).String, Pos2, 1)
        If A$ = "|" Then
            nBars = nBars + 1
            If Pos2 < Leng Then ' check double bar
                B$ = Mid$(gAbcI(V).String, Pos2 + 1, 1)
                If B$ = "|" Then
                    Pos2 = Pos2 + 1
                End If
            End If
            If nBars >= gBarsPerLine Then
                GoSub PutAbcI2
                Exit Do
            End If
        End If
    Loop
    
    If Pos2 >= Leng Then
        If nBars < gBarsPerLine Then
            If nBars > 0 Then ' get end of line
                GoSub PutAbcI2
            End If
        End If
        gAbcI(V).Pos = MAXVAL
    End If
    Return
    
PutAbcI2:
    bTextFound = True
    Text = Text & _
        Mid$(gAbcI(V).String, _
        Pos1, Pos2 - Pos1 + 1)
    gAbcI(V).Pos = Pos2
    GoSub PutFile
    Return

'******************************************
'******************************************

PutFile:
    Print #pOFileNo, Text
    nFile = nFile + 1
    Return

End Sub